Анализ большого XML-файла с помощью XMLPullParser или Sax-Parser в Android приводит к задержкам

У меня возникла следующая проблема: в моем приложении для Android TV я могу добавлять (а затем обновлять) источники epg в виде файлов .xml, .gz или .xz (.gz и .xz распаковываются в .xml). Таким образом, пользователь добавляет URL-адрес файла, он загружается, затем анализируется и сохраняется в базе данных объектного ящика. Попробовал XmlPullParser и Sax-Parser и все работало нормально, для xml размером около 50мб и 700.000 строк (350 каналов и около 80.000 программ) понадобилось:

XmlPullParser -> 50 секунд на эмуляторе, 1 минута 30 секунд прямо на телевизоре Sax-Parser -> 55 секунд на эмуляторе, 1 минута 50 секунд прямо на моем телевизоре

Я предпочитал, чтобы это было немного быстрее, но это было нормально. Затем я впервые понял, что если я обновлю источник epg (снова загрузлю xml, проанализирую его и добавлю новые epgdata в ob-db) и тем временем буду перемещаться по своему приложению,

  1. это длится намного дольше (несколько минут для XmlPullParser и Sax-Parser

  2. приложение начало лагать во время его использования, и на моем телевизоре оно тоже через некоторое время вылетело - вероятно, из-за проблем с памятью. Если бы я обновил источник epg, не делая ничего другого в своем приложении, этого не произошло.

«Исследуя» Профайлер, я заметил две вещи.

  1. При парсинге (особенно программ) сборщик мусора вызывается очень часто, 20-40 раз за 5 секунд.
  2. Когда процесс завершается, Java-часть в профилировщике памяти увеличивается до 200 МБ, и ей требуется некоторое время, прежде чем она получит gc.

Не уверен, но читал, что постоянный вызов сборщика мусора мог вызвать зависания в моем приложении. Поэтому я попытался минимизировать создание объектов, но это как-то ничего не изменило (а может я что-то не так исправил). Я также протестировал этот процесс, не создавая объект базы данных для EpgDataOB, поэтому в базу данных не было добавлено никаких EpgData. Но я все еще мог видеть множество вызовов сборщика мусора в профилировщике, поэтому проблема должна быть в моем коде синтаксического анализа.

Единственное, что мне помогло, это добавление задержки в 100 мс после каждой разбираемой программы (логически это невозможно, поскольку увеличивает время процесса на несколько часов) или уменьшение размера пакета (что также увеличивает время процесса, например: использование размер пакета 500 = время обработки на эмуляторе: 2 минуты 10 секунд, а сборщик мусора вызывается примерно 6-10 раз за 5 секунд, уменьшая пакет до 100 -> эмулятор = почти 3 минуты, gc вызывается 4-5 раз за 5 секунд) .

Выложу обе свои версии.

XmlPullParser

Код репозитория:

 var currentChannel: Channel? = null
    var epgDataBatch = mutableListOf<EpgDataOB>()
    val batchSize = 10000

    suspend fun parseXmlStream(
        inputStream: InputStream,
        epgSourceId: Long,
        maxDays: Int,
        minDays: Int,
        sourceUrl: String
    ): Resource<String> = withContext(Dispatchers.Default) {
        try {
            val thisEpgSource = epgSourceBox.get(epgSourceId)
            val factory = XmlPullParserFactory.newInstance()
            val parser = factory.newPullParser()
            parser.setInput(inputStream, null)
            var eventType = parser.eventType
          
            while (eventType != XmlPullParser.END_DOCUMENT) {
                when (eventType) {
                    XmlPullParser.START_TAG -> {
                        when (parser.name) {
                            "channel" -> {
                                parseChannel(parser, thisEpgSource)
                            }
                            "programme" -> {
                                parseProgram(parser, thisEpgSource)
                            }
                        }
                    }
                }
                eventType = parser.next()
            }
            if (epgDataBatch.isNotEmpty()) {
                epgDataBox.put(epgDataBatch)
            }

            assignEpgDataToChannels(thisEpgSource)

            _epgProcessState.value = ExternEpgProcessState.Success
            Resource.Success("OK")
        } catch (e: Exception) {
            Log.d("ERROR PARSING", "Error parsing XML: ${e.message}")
            _epgProcessState.value = ExternEpgProcessState.Error("Error parsing XML: ${e.message}")
            Resource.Error("Error parsing XML: ${e.message}")
        } finally {
            withContext(Dispatchers.IO) {
                inputStream.close()
            }
        }
    }

    private fun resetChannel() {
        currentChannel = Channel("", mutableListOf(), mutableListOf(), "")
    }

    private fun parseChannel(parser: XmlPullParser, thisEpgSource: EpgSource) {
        resetChannel()
        currentChannel?.id = parser.getAttributeValue(null, "id")

        while (parser.next() != XmlPullParser.END_TAG) {
            if (parser.eventType == XmlPullParser.START_TAG) {
                when (parser.name) {
                    "display-name" -> currentChannel?.displayName = mutableListOf(parser.nextText())
                    "icon" -> currentChannel?.icon = mutableListOf(parser.getAttributeValue(null, "src"))
                    "url" -> currentChannel?.url = parser.nextText()
                }
            }
        }

        val channelInDB = epgChannelBox.query(EpgSourceChannel_.chEpgId.equal("${thisEpgSource.id}_${currentChannel?.id}")).build().findUnique()
        if (channelInDB == null) {
            val epgChannelToAdd = EpgSourceChannel(
                0,
                "${thisEpgSource.id}_${currentChannel?.id}",
                currentChannel?.id ?: "",
                currentChannel?.icon,
                currentChannel?.displayName?.firstOrNull() ?: "",
                thisEpgSource.id,
                currentChannel?.displayName ?: mutableListOf(),
                true
            )
            epgChannelBox.put(epgChannelToAdd)
        } else {
            channelInDB.display_name = currentChannel?.displayName ?: channelInDB.display_name
            channelInDB.icon = currentChannel?.icon
            channelInDB.name = currentChannel?.displayName?.firstOrNull() ?: channelInDB.name
            epgChannelBox.put(channelInDB)
        }
    }

    private fun parseProgram(parser: XmlPullParser, thisEpgSource: EpgSource) {

        val start = SimpleDateFormat("yyyyMMddHHmmss Z", Locale.getDefault())
            .parse(parser.getAttributeValue(null, "start"))?.time ?: -1

        val stop = SimpleDateFormat("yyyyMMddHHmmss Z", Locale.getDefault())
            .parse(parser.getAttributeValue(null, "stop"))?.time ?: -1

        val channel = parser.getAttributeValue(null, "channel")

        val isAnUpdate = if (isUpdating) {
            epgDataBox.query(EpgDataOB_.idByAccountData.equal("${channel}_${start}_${thisEpgSource.id}")).build().findUnique() != null
        } else {
            false
        }

        if (!isAnUpdate) {
            val newEpgData = EpgDataOB(
                id = 0, 
                idByAccountData = "${channel}_${start}_${thisEpgSource.id}",
                epgId = channel ?: "",
                chId = channel ?: "",
                datum = SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()).format(start),
                name = "",
                sub_title = "",
                descr = "",
                category = null,
                director = null,
                actor = null,
                date = "",
                country = null,
                showIcon = "",
                episode_num = "",
                rating = "",
                startTimestamp = start,
                stopTimestamp = stop,
                mark_archive = null,
                accountData = thisEpgSource.url,
                epgSourceId = thisEpgSource.id.toInt(),
                epChId = "${thisEpgSource.id}_${channel}"
            )
     
            while (parser.next() != XmlPullParser.END_TAG) {
                if (parser.eventType == XmlPullParser.START_TAG) {
                    when (parser.name) {
                        "title" -> newEpgData.name = parser.nextText()
                        "sub-title" -> newEpgData.sub_title = parser.nextText()
                        "desc" -> newEpgData.descr = parser.nextText()
                        "director" -> newEpgData.director?.add(parser.nextText())
                        "actor" -> newEpgData.actor?.add(parser.nextText())
                        "date" -> newEpgData.date = parser.nextText()
                        "category" -> newEpgData.category?.add(parser.nextText())
                        "country" -> newEpgData.country?.add(parser.nextText())
                        "episode-num" -> newEpgData.episode_num = parser.nextText()
                        "value" -> newEpgData.rating = parser.nextText()
                        "icon" -> newEpgData.showIcon = parser.getAttributeValue(null, "src") ?: ""
                    }
                }
            }

            epgDataBatch.add(newEpgData)
            if (epgDataBatch.size >= batchSize) {
                epgDataBox.put(epgDataBatch)
                epgDataBatch.clear()
            }
        }
    }

    private fun assignEpgDataToChannels(thisEpgSource: EpgSource) {
        epgChannelBox.query(EpgSourceChannel_.epgSourceId.equal(thisEpgSource.id)).build().find().forEach { epgChannel ->
            epgChannel.epgSource.target = thisEpgSource
            epgChannel.epgDataList.addAll(epgDataBox.query(EpgDataOB_.epChId.equal(epgChannel.chEpgId)).build().find())
            epgChannelBox.put(epgChannel)
        }
        epgDataBatch.clear()
    }

Саксофонный парсер

Код репозитория:

suspend fun parseXmlStream(
        inputStream: InputStream,
        epgSourceId: Long,
        maxDays: Int,
        minDays: Int,
        sourceUrl: String
    ): Resource<String> = withContext(Dispatchers.Default) {
        try {
            val thisEpgSource = epgSourceBox.get(epgSourceId)
            inputStream.use { input ->
                val saxParserFactory = SAXParserFactory.newInstance()
                val saxParser = saxParserFactory.newSAXParser()
                val handler = EpgSaxHandler(thisEpgSource.id, maxDays, minDays, thisEpgSource.url, isUpdating)
                saxParser.parse(input, handler)
                if (handler.epgDataBatch.isNotEmpty()) {
                    epgDataBox.put(handler.epgDataBatch)
                    handler.epgDataBatch.clear()
                }
                _epgProcessState.value = ExternEpgProcessState.Success
                return@withContext Resource.Success("OK")
            }
        } catch (e: Exception) {
            Log.e("ERROR PARSING", "${e.message}")
            _epgProcessState.value = ExternEpgProcessState.Error("Error parsing XML: ${e.message}")
            return@withContext Resource.Error("Error parsing XML: ${e.message}")
        }
    }

Обработчик:

class EpgSaxHandler(
    private val epgSourceId: Long,
    private val maxDays: Int,
    private val minDays: Int,
    private val sourceUrl: String,
    private val isUpdating: Boolean
) : DefaultHandler() {

    private val epgSourceBox: Box<EpgSource>
    private val epgChannelBox: Box<EpgSourceChannel>
    private val epgDataBox: Box<EpgDataOB>


    init {
        val store = ObjectBox.store
        epgSourceBox = store.boxFor(EpgSource::class.java)
        epgChannelBox = store.boxFor(EpgSourceChannel::class.java)
        epgDataBox = store.boxFor(EpgDataOB::class.java)
    }

    var epgDataBatch = mutableListOf<EpgDataOB>()
    private val batchSize = 10000
    private var currentElement = ""
    private var currentChannel: Channel? = null
    private var currentProgram: EpgDataOB? = null
    private var stringBuilder = StringBuilder()


    override fun startElement(uri: String?, localName: String?, qName: String?, attributes: Attributes?) {
        currentElement = qName ?: ""
        when (qName) {
            "channel" -> {
                val id = attributes?.getValue("id") ?: ""
                currentChannel = Channel(id, mutableListOf(), mutableListOf(), "")
            }
            "programme" -> {

                val start = SimpleDateFormat("yyyyMMddHHmmss Z", Locale.getDefault())
                    .parse(attributes?.getValue("start") ?: "")?.time ?: -1

                val stop = SimpleDateFormat("yyyyMMddHHmmss Z", Locale.getDefault())
                    .parse(attributes?.getValue("stop") ?: "")?.time ?: -1

                val channel = attributes?.getValue("channel") ?: ""

                if (isUpdating) {
                    val existingProgram = epgDataBox.query(EpgDataOB_.idByAccountData.equal("${channel}_${start}_${epgSourceId}",)).build().findUnique()
                    if (existingProgram != null) {
                        currentProgram = null
                        return
                    }
                }
                currentProgram = EpgDataOB(
                    id = 0,
                    idByAccountData = "${channel}_${start}_${epgSourceId}",
                    epgId = channel,
                    chId = channel,
                    datum = SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()).format(start),
                    name = "",
                    sub_title = "",
                    descr = "",
                    category = mutableListOf(),
                    director = mutableListOf(),
                    actor = mutableListOf(),
                    date = "",
                    country = mutableListOf(),
                    showIcon = "",
                    episode_num = "",
                    rating = "",
                    startTimestamp = start,
                    stopTimestamp = stop,
                    mark_archive = null,
                    accountData = sourceUrl,
                    epgSourceId = epgSourceId.toInt(),
                    epChId = "${epgSourceId}_$channel"
                )
            }
            "icon" -> {
                val src = attributes?.getValue("src") ?: ""
                currentChannel?.icon?.add(src)
                currentProgram?.showIcon = src
            }
            "desc", "title", "sub-title", "episode-num", "rating", "country", "director", "actor", "date", "display-name" -> {
                stringBuilder = StringBuilder()
            }
        }
    }

    override fun characters(ch: CharArray?, start: Int, length: Int) {
        ch?.let {
            stringBuilder.append(it, start, length)
        }
    }

    override fun endElement(uri: String?, localName: String?, qName: String?) {
        when (qName) {
            "channel" -> {
                currentChannel?.let { channel ->
                    val channelInDB = epgChannelBox.query(EpgSourceChannel_.chEpgId.equal("${epgSourceId}_${channel.id}")).build().findUnique()
                    if (channelInDB == null) {
                        val newChannel = EpgSourceChannel(
                            id = 0,
                            chEpgId = "${epgSourceId}_${channel.id}",
                            chId = channel.id,
                            icon = channel.icon,
                            display_name = channel.displayName,
                            name = channel.displayName.firstOrNull() ?: "",
                            epgSourceId = epgSourceId,
                            isExternalEpg = true
                        )
                        epgChannelBox.put(newChannel)
                    } else {
                        channelInDB.display_name = channel.displayName
                        channelInDB.icon = channel.icon
                        channelInDB.name = channel.displayName.firstOrNull() ?: channelInDB.name
                        epgChannelBox.put(channelInDB)
                    }
                }
                currentChannel = null
            }
            "programme" -> {
                currentProgram?.let { program ->
                    addEpgDataToBatch(program)
                }
                currentProgram = null
            }
            "desc" -> {
                currentProgram?.descr = stringBuilder.toString()
            }
            "title" -> {
                currentProgram?.name = stringBuilder.toString()
            }
            "sub-title" -> {
                currentProgram?.sub_title = stringBuilder.toString()
            }
            "episode-num" -> {
                currentProgram?.episode_num = stringBuilder.toString()
            }
            "rating" -> {
                currentProgram?.rating = stringBuilder.toString()
            }
            "country" -> {
                currentProgram?.country?.add(stringBuilder.toString())
            }
            "director" -> {
                currentProgram?.director?.add(stringBuilder.toString())
            }
            "actor" -> {
                currentProgram?.actor?.add(stringBuilder.toString())
            }
            "date" -> {
                currentProgram?.date = stringBuilder.toString()
            }
            "display-name" -> {
                currentChannel?.displayName?.add(stringBuilder.toString())
            }
        }
        currentElement = ""
    }



    private fun addEpgDataToBatch(epgData: EpgDataOB) {
        epgDataBatch.add(epgData)
        if (epgDataBatch.size >= batchSize) {
            processEpgDataBatch()
        }
    }

    private fun processEpgDataBatch() {
        if (epgDataBatch.isNotEmpty()) {
            epgDataBox.put(epgDataBatch)
            epgDataBatch.clear()
        }
    }
}

Поэтому я ищу быстрый способ проанализировать XML-данные и вставить их в базу данных без лагов или сбоев в моем приложении :-):-) Что-то не так в моем коде, что вызывает задержки? Или это просто невозможно, не замедляя процесс синтаксического анализа и вставки базы данных?

Если нужен какой-либо другой код, я могу опубликовать его. Вот как выглядит профилировщик памяти при анализе программ с помощью XmlPullParser:

ОБНОВЛЯТЬ:

Использование памяти и gc -> только синтаксический анализ, без использования базы данных Я использовал классы данных Channel & Program для анализа где-то данных и всегда повторно использовал один и тот же канал/программу:

Использование памяти и gc → синтаксический анализ и создание объектов EpgDataOB (без вставки базы данных)

Использование памяти и gc -> синтаксический анализ и добавление данных в базу данных (db = последние 10 секунд)

Использование памяти и gc -> синтаксический анализ, добавление данных в базу данных и управление связью epg-канала со списком EpgData с помощью:

 private fun addEpgDataToDatabase() {
        GlobalScope.launch {
            withContext(Dispatchers.IO) {
                epgDataBatch.chunked(15000).forEach { batch ->
                    epgDataBox.put(batch)
                    epgChannelBatch.forEach { epgChannel ->
                        epgChannel.epgDataList.addAll(batch.filter { it.epChId == epgChannel.chEpgId })
                    }
                    Log.d("EPGPARSING ADD TO DB", "OK")
                    delay(500)
                }
                epgDataBatch.clear()
            }
        }
    }

Новый код для помещения разобранных данных в данные (проверено также 3 раза на ТВ, работает намного лучше, чем с кодом моего вопроса). Добавление всего epgDataBatch (= mutableListof) с одним помещенным в базу данных происходит даже немного быстрее.

 private fun addEpgDataToDatabase() {
        epgDataBatch.chunked(30000).forEach { batch ->
            epgDataBox.store.runInTx {
                epgDataBox.put(batch)
                epgDataBox.closeThreadResources()
            }
        }
        addEpgDataToChannel()
    }

    private fun addEpgDataToChannel() {
        epgChannelBox.store.runInTx {
            for (epgCh in epgChannelBatch) {
                epgCh.epgDataList.addAll(epgDataBatch.filter { it.epChId == epgCh.chEpgId })
            }
            epgChannelBox.put(epgChannelBatch)
            epgChannelBox.closeThreadResources()
        }
        epgChannelBatch.clear()
        epgDataBatch.clear()
    }

О каком типе базы данных мы говорим? Вы уверены, что проблема в реализации синтаксического анализа (вы пробовали анализировать, не сохраняя его в базе данных)?

Robert 17.07.2024 21:15

В продолжение того, что сказал @Robert: если вы делаете много вставок в базу данных, это может быть очень дорого, потому что каждая из них имеет неявную транзакцию, которая является дорогостоящей. Лучше контролировать транзакцию самостоятельно и начинать только одну перед анализом и завершать ее после завершения.

Andrew 17.07.2024 21:35

Я использую объектную базу данных. Я обновил вопрос, добавив некоторые тесты (и код), которые я сделал. Стабилен только синтаксический анализ = использование памяти, но gc вызывается очень часто. Добавление данных в базу данных логически увеличивает использование памяти (последнее изображение в разделе «Обновить»). Но поскольку мне нужны все данные в БД и связи, я не вижу другого способа справиться с этим? @Андрей, что ты имеешь в виду под «самостоятельно контролировать транзакцию и начинать только одну перед синтаксическим анализом и завершать ее после завершения»? Итак, вы оба думаете, что мое приложение тормозит не вызовы сборщика мусора, а операции с базой данных, которые я использовал при синтаксическом анализе?

Alex Mutschl 18.07.2024 11:35

Еще вопрос: частые вызовы сборщика мусора при разборе больших xml-файлов - это нормально? Разве они не мешают пользовательскому опыту?

Alex Mutschl 18.07.2024 11:37

См. docs.objectbox.io/transactions#transaction-costs каждый раз, когда вы выполняете put блокировку ввода-вывода, что может быть вредно для сопрограммы, возможно, стоит профилировать процессор, поскольку сопрограммы менее понятны. мне по влиянию на основную ветку. Я анализировал большой файл csv и переход от транзакции на вставку (а не к объектному блоку) к одной для каждого файла значительно улучшил скорость. Objectbox немного странный, но предлагает objectbox.io/docfiles/java/current/io/objectbox/…

Andrew 18.07.2024 11:53

Вероятно, первое, что нужно сделать, — определить количество баз данных put (вставок/записей), чтобы увидеть, есть ли у вас большое количество, которое повлияет на производительность. Затем в качестве теста временно закомментируйте всю базу данных put, чтобы посмотреть, какой может быть производительность, если убрать стоимость многих транзакций. Кажется, что объектный блок предлагает при синтаксическом анализе хранить данные в массиве памяти и просто выполнять один большой put (дорогостоящий в памяти) или спроектировать весь анализ как Runnable (в потоке) и не использовать сопрограммы с BoxStore.runInTx()

Andrew 18.07.2024 12:33

Изменены анализируемые данные -> код базы данных; добавление всех каналов и всех epgdata в отдельные списки. После анализа я приступаю к вставке epgdata и каналов в базу данных, используя BoxStore.rundInTx() (значительно ускорил процесс -> общий процесс длится 20-30 секунд на эмуляторе). Вставка увеличивает использование памяти, но вроде нормально. Единственное, что сейчас вызывает некоторые проблемы, - это собственная часть памяти, которая увеличивается с высотой пакета (одна транзакция для всех данных = 130 МБ) и не освобождается, пока я не перезапущу приложение. Сегодня вечером тоже проверю прямо по телевизору.

Alex Mutschl 18.07.2024 16:07

Я добавил ответ о снижении транзакционных издержек вашей базы данных put, чтобы он не потерялся в комментариях, поскольку это улучшило скорость вашего кода синтаксического анализа.

Andrew 18.07.2024 20:43
0
8
68
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Вставки в базу данных могут быть дорогостоящими, если вы выполняете их много при вставке проанализированных XML-данных после объекта данных. Из документации ObjectBox.

Это связано с тем, что он использует блокировку ввода-вывода и блокировки файлов для записи базы данных на диск, поскольку каждый оператор put находится в неявной транзакции.

Таким образом, вы можете ускорить анализ, ускорив вставку в базу данных.

Вы можете объединить данные в массив и put (вставить) их все за один раз и, таким образом, выполнить только одну транзакцию, это потребует больше памяти, но будет быстрее.

Или у ObjectBox есть BoxStore.runInTx(), который использует Runnable для выполнения нескольких операций put в одной транзакции.

Кажется, ObjectBox хочет, чтобы вы не начинали транзакцию в начале синтаксического анализа xml и не завершали ее после завершения синтаксического анализа xml. Для этого у него есть внутренний метод низкого уровня.

Обратите внимание, что это также относится и к другим файловым базам данных, таким как sqlite.

Спасибо за ваше объяснение. Сначала я проанализировал все данные и сохранил каналы epg в mutableList, а все данные epg — в mutableList. Затем я попробовал оба варианта: 1. добавление всех epgdata за один раз было очень быстрым, но размер встроенной части профилировщика увеличился до 130-140 МБ, и она не освобождалась до тех пор, пока я не перезапустил приложение. 2. Я разбил mutableList EpgData на пакеты по 30 000 (всего 80 000 epgdata) и использовал runInTx(), тоже было быстро, а Native-часть профилировщика увеличилась «только» до 90 МБ, но она тоже не была освобождена, только с перезапуском приложения. Разве это не должно быть освобождено через некоторое время?

Alex Mutschl 18.07.2024 22:05

Я не эксперт по внутреннему устройству ObjectBox, но, вероятно, освобождаюсь, когда вы BoxStore.close()

Andrew 18.07.2024 22:20

Читал об этом и уже пробовал, но возникли некоторые проблемы во фрагменте, вызывающем функции обновления источника epg. На следующей неделе проведу тестирование, если получу решение, опубликую его здесь. Добавил свой код для использования пакетов в вопросе внизу. У меня еще один вопрос, сборки мусора, которые выполняются во время парсинга, можно игнорировать? (даже если их так много)

Alex Mutschl 18.07.2024 22:27

Обычно GC работает в своем собственном потоке, поэтому не следует замедлять работу, если только у вас не работает слишком много других потоков, выполняющих слишком большую работу.

Andrew 18.07.2024 23:07

Ответ Эндрюса обобщил причину моей проблемы. Я должен был исправить код синтаксического анализа, но не осознавал, что проблему могут вызвать вставки в базу данных, а не синтаксический анализ и сборщик мусора. По поводу проблемы с увеличением нативной части при вставке всех epgdata в базу данных я задал новый вопрос, так как это отдельный вопрос, см.: https://stackoverflow.com/questions/78783374/..

Alex Mutschl 23.07.2024 14:21

Другие вопросы по теме