Export: jump to "from date" instead of walking entire chat history#30727
Export: jump to "from date" instead of walking entire chat history#30727Lertov2424232 wants to merge 3 commits into
Conversation
092723e to
58d704b
Compare
|
Thanks @23rd — yes, all three PRs are tackling the same root issue (the exporter walks the whole chat from #30727 (this PR)
On boundary semantics
The previous revision of this PR passed
So this PR should now be a strict superset of the export-API correctness changes in #30612, while staying surgical (no UI / settings churn). Happy to defer to whichever PR you ultimately prefer — if you'd rather take #30612 or #30618 as the base I'm fine to close this and open a small follow-up with whatever the chosen base is missing instead. Just let me know. |
|
Did you (as human person) test your changes in some cases? |
When the user picks a "From date" for chat / topic export, the exporter still walked messages.getHistory from `offset_id = 1` (i.e. from the very first message of the chat) and filtered each message client-side via `SkipMessageByDate`. On a chat with millions of messages that meant tens of thousands of redundant API calls before reaching the requested range. Use MTP's `offset_date` parameter on the first slice of each split / topic to position the server directly at `singlePeerFrom`, then continue the normal forward walk by `offset_id` once we have a foothold. Additional changes: * `messages.Search` (onlyMyMessages branch) now passes `min_date = singlePeerFrom` and `max_date = singlePeerTill`, which were previously hardcoded to 0. * `finishMessagesSlice` / `finishTopicMessagesSlice` early-stop the loop once the newest message in the current batch is at or past `singlePeerTill`, instead of walking to the end of the chat just to client-side-skip the tail. * `TopicProcess` gains a `firstSliceRequested` flag so the topic loop can cleanly distinguish the first slice request from continuation (the existing `offsetId == 0` check could not, since `offsetId` is seeded with `topicRootId`). Behaviour with no date filter (`singlePeerFrom == 0 && singlePeerTill == 0`) is unchanged byte-for-byte; the new branches only activate when the user explicitly picks a date range in the export dialog.
Address edge cases on top of the offset_date jump: * SingleMessageAfter: switch the comparison to >= so a split whose newest message has date == singlePeerFrom is not pre-skipped by the count probe. SkipMessageByDate keeps messages with singlePeerFrom <= date, so the pre-iteration filter must match that inclusive lower bound. * requestChatMessages (onlyMyMessages branch): messages.search uses strict > for min_date and strict < for max_date, while the exporter's canonical range is [singlePeerFrom, singlePeerTill). Pass min_date = singlePeerFrom - 1 to keep messages dated exactly singlePeerFrom; max_date stays singlePeerTill. * loadNextTopicMessageFile: skip media download for messages outside the date range, mirroring loadNextMessageFile for the chat path. Without this, topic exports with a date filter still trigger downloads for out-of-range messages. * changelog.txt: add a user-facing entry.
a052299 to
29db441
Compare
|
Re: "Did you (as human person) test your changes in some cases?" Honest answer: I couldn't get an end-to-end Telegram Desktop build done. My local machine doesn't have the ~60-80 GB of free disk required for the Windows build per Rather than ship something un-tested, I wrote a small Python simulator that models the relevant pieces of the export state machine plus the
Code + raw output: https://gist.github.com/Lertov2424232/39dc5cd04f033914a9369905c1fc9ada Summary of results
SingleMessageAfter boundary (direct check)The count-probe pre-filter ( This is why the original Limitations of this evidence
AskWould you be willing to approve CI on this PR so it can run against your warm libraries cache? My fork couldn't get past the cold-cache lzma issue and I'd much rather submit empirical evidence than a simulator. I'm also happy to drop any of the follow-up fixes if you'd prefer them as separate PRs. |
🤣 |
|
Someone's OpenClaw? Did any of PRs do the correct progress reporting on limited-by-date range exports? |
When the user picks a date range, ask the server for the exact count of messages matching the range via messages.search instead of using the full split count. This makes the export progress bar reflect real progress for date-limited exports (previously it stored the whole-chat count and jumped from ~0% to 100% near the end). The same per-split search call also lets us drop the SingleMessageAfter / SingleMessageBefore probes - if the matching count is zero, the split is empty for the picked range and is skipped. Non-date-limited exports are unchanged: requestMessagesCount keeps using messages.getHistory with limit=1 to read the split count.
| splitRequest(realSplitIndex, MTPmessages_Search( | ||
| MTP_flags(flags), | ||
| realPeerInput, | ||
| MTP_string(), // query | ||
| fromInput, | ||
| MTPInputPeer(), // saved_peer_id | ||
| MTPVector<MTPReaction>(), // saved_reaction | ||
| MTPint(), // top_msg_id | ||
| MTP_inputMessagesFilterEmpty(), | ||
| MTP_int(minDate), // min_date | ||
| MTP_int(maxDate), // max_date | ||
| MTP_int(0), // offset_id | ||
| MTP_int(0), // add_offset | ||
| MTP_int(1), // limit - we only need .count | ||
| MTP_int(0), // max_id | ||
| MTP_int(0), // min_id | ||
| MTP_long(0) // hash | ||
| )).done([=](const MTPmessages_Messages &result) { |
Problem
When the user picks a "From date" for chat / topic export, the exporter still issues
messages.getHistorystarting atoffset_id = 1(the very first message of the chat) and walks forward through the entire history, filtering each message client-side viaSkipMessageByDate. On a chat with millions of messages this is tens of thousands of redundant API calls and tens of millions of messages transferred just to discard them. Users have reported export effectively hanging for hours on large chats when only the last few days were requested.Fix
Use MTP's
offset_dateparameter on the first slice of each split / topic to position the server directly atsinglePeerFrom, then continue the existing forward walk byoffset_idonce we have a foothold. The flow is otherwise unchanged.Changes
requestMessagesSlice()— on the first slice of a split, ifsinglePeerFrom > 0, request withoffset_id = 0, offset_date = singlePeerFrom, add_offset = -limit, limit = kMessagesSliceLimit. Subsequent slices use the existinglargestIdPlusOnewalk.requestChatMessages()— gains an optionaloffsetDateparameter (default0), threaded into theMTPmessages_GetHistorycall (which previously hardcodedoffset_date = 0). The CHANNEL_PRIVATE →onlyMyMessagesfallback recurses with the sameoffsetDate.messages.Search(theonlyMyMessagesbranch) now passesmin_date = singlePeerFromandmax_date = singlePeerTill, which were previously hardcoded to0.finishMessagesSlice()/finishTopicMessagesSlice()— early-stop the loop once the newest message in the current batch is at or pastsinglePeerTill, instead of walking to the end of the chat just to client-side-skip the tail.requestTopicMessagesSlice()/requestTopicReplies()— sameoffset_datetreatment for forum topic export.TopicProcessgains afirstSliceRequestedflag so the topic loop can cleanly distinguish the first slice request from continuation (the existingoffsetId == 0check could not, sinceoffsetIdis seeded withtopicRootIdafter the root-message fetch).Behaviour without a date filter
When
singlePeerFrom == 0 && singlePeerTill == 0the new branches are all gated off and the export performs the same MTP calls as before, byte-for-byte. The new code paths activate only when the user explicitly picks a date range in the export dialog.Verification
A small Python simulator of the relevant
messages.getHistorycall shapes was used to verify that:from= last 500 msgsfrom+tillslotfromin futurefromat oldest messagetillolder than oldestThe simulator script is not included in this PR — happy to add it as a developer tool / test if maintainers want it.
Known follow-up (not in this PR)
_chatProcess->info.messagesCountPerSplit[*]for thegetHistorypath still reflects the total message count of the chat (not just the in-range count), so the export progress bar percentage in the UI is computed against the full chat. The export itself terminates correctly (lastSlicefrom the server, or the new early-stop onsinglePeerTill), but the visible percentage caps below 100% on a narrow range. Fixing this cleanly would require either a server-side count probe withmin_date/max_date(viamessages.Search) or counting processed messages locally, which is a larger change in the precount pipeline. Left out to keep this PR surgical.