У меня возникла следующая проблема: в моем приложении для 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) и тем временем буду перемещаться по своему приложению,
это длится намного дольше (несколько минут для XmlPullParser и Sax-Parser
приложение начало лагать во время его использования, и на моем телевизоре оно тоже через некоторое время вылетело - вероятно, из-за проблем с памятью. Если бы я обновил источник epg, не делая ничего другого в своем приложении, этого не произошло.
«Исследуя» Профайлер, я заметил две вещи.
Не уверен, но читал, что постоянный вызов сборщика мусора мог вызвать зависания в моем приложении. Поэтому я попытался минимизировать создание объектов, но это как-то ничего не изменило (а может я что-то не так исправил). Я также протестировал этот процесс, не создавая объект базы данных для 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: если вы делаете много вставок в базу данных, это может быть очень дорого, потому что каждая из них имеет неявную транзакцию, которая является дорогостоящей. Лучше контролировать транзакцию самостоятельно и начинать только одну перед анализом и завершать ее после завершения.
Я использую объектную базу данных. Я обновил вопрос, добавив некоторые тесты (и код), которые я сделал. Стабилен только синтаксический анализ = использование памяти, но gc вызывается очень часто. Добавление данных в базу данных логически увеличивает использование памяти (последнее изображение в разделе «Обновить»). Но поскольку мне нужны все данные в БД и связи, я не вижу другого способа справиться с этим? @Андрей, что ты имеешь в виду под «самостоятельно контролировать транзакцию и начинать только одну перед синтаксическим анализом и завершать ее после завершения»? Итак, вы оба думаете, что мое приложение тормозит не вызовы сборщика мусора, а операции с базой данных, которые я использовал при синтаксическом анализе?
Еще вопрос: частые вызовы сборщика мусора при разборе больших xml-файлов - это нормально? Разве они не мешают пользовательскому опыту?
См. docs.objectbox.io/transactions#transaction-costs каждый раз, когда вы выполняете put
блокировку ввода-вывода, что может быть вредно для сопрограммы, возможно, стоит профилировать процессор, поскольку сопрограммы менее понятны. мне по влиянию на основную ветку. Я анализировал большой файл csv и переход от транзакции на вставку (а не к объектному блоку) к одной для каждого файла значительно улучшил скорость. Objectbox немного странный, но предлагает objectbox.io/docfiles/java/current/io/objectbox/…
Вероятно, первое, что нужно сделать, — определить количество баз данных put
(вставок/записей), чтобы увидеть, есть ли у вас большое количество, которое повлияет на производительность. Затем в качестве теста временно закомментируйте всю базу данных put
, чтобы посмотреть, какой может быть производительность, если убрать стоимость многих транзакций. Кажется, что объектный блок предлагает при синтаксическом анализе хранить данные в массиве памяти и просто выполнять один большой put
(дорогостоящий в памяти) или спроектировать весь анализ как Runnable (в потоке) и не использовать сопрограммы с BoxStore.runInTx()
Изменены анализируемые данные -> код базы данных; добавление всех каналов и всех epgdata в отдельные списки. После анализа я приступаю к вставке epgdata и каналов в базу данных, используя BoxStore.rundInTx() (значительно ускорил процесс -> общий процесс длится 20-30 секунд на эмуляторе). Вставка увеличивает использование памяти, но вроде нормально. Единственное, что сейчас вызывает некоторые проблемы, - это собственная часть памяти, которая увеличивается с высотой пакета (одна транзакция для всех данных = 130 МБ) и не освобождается, пока я не перезапущу приложение. Сегодня вечером тоже проверю прямо по телевизору.
Я добавил ответ о снижении транзакционных издержек вашей базы данных put
, чтобы он не потерялся в комментариях, поскольку это улучшило скорость вашего кода синтаксического анализа.
Вставки в базу данных могут быть дорогостоящими, если вы выполняете их много при вставке проанализированных 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 МБ, но она тоже не была освобождена, только с перезапуском приложения. Разве это не должно быть освобождено через некоторое время?
Я не эксперт по внутреннему устройству ObjectBox, но, вероятно, освобождаюсь, когда вы BoxStore.close()
Читал об этом и уже пробовал, но возникли некоторые проблемы во фрагменте, вызывающем функции обновления источника epg. На следующей неделе проведу тестирование, если получу решение, опубликую его здесь. Добавил свой код для использования пакетов в вопросе внизу. У меня еще один вопрос, сборки мусора, которые выполняются во время парсинга, можно игнорировать? (даже если их так много)
Обычно GC работает в своем собственном потоке, поэтому не следует замедлять работу, если только у вас не работает слишком много других потоков, выполняющих слишком большую работу.
Ответ Эндрюса обобщил причину моей проблемы. Я должен был исправить код синтаксического анализа, но не осознавал, что проблему могут вызвать вставки в базу данных, а не синтаксический анализ и сборщик мусора. По поводу проблемы с увеличением нативной части при вставке всех epgdata в базу данных я задал новый вопрос, так как это отдельный вопрос, см.: https://stackoverflow.com/questions/78783374/..
О каком типе базы данных мы говорим? Вы уверены, что проблема в реализации синтаксического анализа (вы пробовали анализировать, не сохраняя его в базе данных)?