UnigramDev/Unigram/develop • 0ef54e8 • 4 files, +110/-27
Parse the receive path off TDLib's buffer, behind TdPointerParser
Client.Receive and Client.Execute hand td_receive's pointer straight to
ClientJson.FromPtr: no copy into a managed buffer, and no byte-at-a-time scan
for the terminator, which was 9-12% of the old parse and would have been a
fifth of this one. TdJsonReader needs no length when the buffer is
NUL-terminated - TdJsonReader.NulTerminated says so at the call site - which
is what deletes the scan without patching td_receive to return it.
Both are alternatives, not replacements: the Utf8JsonReader code is still
there in the #else, and one property drives both halves. TdPointerParser sets
the constant Client.cs reads and turns on the generator mode that emits the
parsers it calls, so false puts the whole thing back. FromJson_* is emitted
either way and still parses what the instant view editor hands back.
Not built - Telegram.csproj is not buildable here. Client.cs, ClientJson.cs,
PtrClientJson.cs and TdJsonReader.cs do compile both ways, checked by a
throwaway project outside the repo; ClientService and everything downstream of
Receive have only been read.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
UnigramDev/Unigram/develop • 0579837 • 5 files, +40/-6
Put the last three FromJson callers behind one string entry point
RichHtml and RichEditorCommands parse JSON that arrives as text from the
instant view editor, and each did its own Encoding.UTF8.GetBytes into a span.
ClientJson.FromJson(string) does it once, and follows TD_POINTER_PARSER like
everything else: it encodes one byte longer than the text and hands the
pointer reader its terminator, or falls back to the span overload.
These were the only callers of the Utf8JsonReader parsers left outside
Client.cs, so this is what has to happen before that set can be dropped from
the app altogether.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
UnigramDev/Unigram/develop • e38afd9 • 8 files, +253/-9
Count what Client.Receive actually deserializes
The benchmark measures the parser over a fixed corpus in a loop, which says
how fast it can go and nothing about how much of it a session asks for.
TdThroughput counts updates, bytes and time inside the parse, and reports that
time as a share of wall clock - the number that decides whether the 2.4x is
worth anything outside a benchmark.
A runtime switch rather than [Conditional] like Instrumentation and Profiler,
because the interesting build is .NET Native Release, which is exactly the
configuration that never defines INSTRUMENTATION. Off it costs a static read
per update. On it costs two timestamps, plus a scan for the payload length on
the pointer path, which happens after the interval is closed so it slows
ingestion rather than flattering the rate.
Diagnostics > TDLib JSON turns it on and shows it; the counters are read when
the page opens rather than bound live, since a ticking readout would be a
second observer of the thread being measured.
Not built.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
#unigram
Parse the receive path off TDLib's buffer, behind TdPointerParser
Client.Receive and Client.Execute hand td_receive's pointer straight to
ClientJson.FromPtr: no copy into a managed buffer, and no byte-at-a-time scan
for the terminator, which was 9-12% of the old parse and would have been a
fifth of this one. TdJsonReader needs no length when the buffer is
NUL-terminated - TdJsonReader.NulTerminated says so at the call site - which
is what deletes the scan without patching td_receive to return it.
Both are alternatives, not replacements: the Utf8JsonReader code is still
there in the #else, and one property drives both halves. TdPointerParser sets
the constant Client.cs reads and turns on the generator mode that emits the
parsers it calls, so false puts the whole thing back. FromJson_* is emitted
either way and still parses what the instant view editor hands back.
Not built - Telegram.csproj is not buildable here. Client.cs, ClientJson.cs,
PtrClientJson.cs and TdJsonReader.cs do compile both ways, checked by a
throwaway project outside the repo; ClientService and everything downstream of
Receive have only been read.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
UnigramDev/Unigram/develop • 0579837 • 5 files, +40/-6
Put the last three FromJson callers behind one string entry point
RichHtml and RichEditorCommands parse JSON that arrives as text from the
instant view editor, and each did its own Encoding.UTF8.GetBytes into a span.
ClientJson.FromJson(string) does it once, and follows TD_POINTER_PARSER like
everything else: it encodes one byte longer than the text and hands the
pointer reader its terminator, or falls back to the span overload.
These were the only callers of the Utf8JsonReader parsers left outside
Client.cs, so this is what has to happen before that set can be dropped from
the app altogether.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
UnigramDev/Unigram/develop • e38afd9 • 8 files, +253/-9
Count what Client.Receive actually deserializes
The benchmark measures the parser over a fixed corpus in a loop, which says
how fast it can go and nothing about how much of it a session asks for.
TdThroughput counts updates, bytes and time inside the parse, and reports that
time as a share of wall clock - the number that decides whether the 2.4x is
worth anything outside a benchmark.
A runtime switch rather than [Conditional] like Instrumentation and Profiler,
because the interesting build is .NET Native Release, which is exactly the
configuration that never defines INSTRUMENTATION. Off it costs a static read
per update. On it costs two timestamps, plus a scan for the payload length on
the pointer path, which happens after the interval is closed so it slows
ingestion rather than flattering the rate.
Diagnostics > TDLib JSON turns it on and shows it; the counters are read when
the page opens rather than bound live, since a ticking readout would be a
second observer of the thread being measured.
Not built.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
#unigram
UnigramDev/Unigram/develop • b2ec787 • 1 files, +6/-0
Carrying both parsers costs compile time, not binary size
Three x64 bundles built the same way: Reader 77,228,975 bytes, Pointer
77,754,662, Both 77,749,472. Both and Pointer are within 5 KB, because ILC
strips the set nothing calls. The 526 KB between Reader and Pointer is the
pointer parsers being larger native code, the same 40% the line counts show.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
UnigramDev/Unigram/develop • 85ee4c2 • 3 files, +131/-19
Register CompositionTarget.Rendering per view
Verified by Fela: video and lottie both work in the call window again.
CsWinRT keeps one event source per statics object for the whole process, so a
second view's CompositionTarget.Rendering += never reaches add_Rendering - the
delegate is appended to the registration the first view made, and the handler runs
on that view's thread. Every secondary view's frame callback lands on the main
view's thread. Composition objects do not mind, being agile, which is why the call
blobs animated throughout; WriteableBitmap does mind, and DrawFrame died on
RPC_E_WRONG_THREAD.
CompositionTargetRendering registers through the ABI instead, so each view gets a
registration of its own. The statics object is a process-wide agile singleton -
measured, along with its IAgileObject - so the call runs on the calling thread and
registers there. Proved in a throwaway app first: in the same run, the projected
subscription fired on view 1's thread and the ABI one on view 2's.
Reported as https://github.com/microsoft/CsWinRT/issues/2524.
Also drops the marshalling branch added to RegisterRendering earlier, which was
aimed at the wrong cause and never once fired; the presenter's own _loader stays.
Left alone, and noted in the todo: CompositionVSync, VisualUtilities,
PremiumProgressBar, PremiumSlider and GiftCraftPopup still subscribe through the
projection. Same latent bug, invisible so far because they only touch Composition.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
UnigramDev/Unigram/develop • 388e015 • 8 files, +114/-66
Alias CompositionTarget rather than #if every call site
Fela's idea, and a better one: CsWinRT.cs already keeps the global usings, so the
stand-in goes in there and the twenty call sites stay written the way they were.
#if NET9_0_OR_GREATER
global using CompositionTarget = Telegram.Common.CompositionTargetImpl;
#else
global using CompositionTarget = Windows.UI.Xaml.Media.CompositionTarget;
#endif
Aliasing in both directions pays for itself twice: .NET Native binds to the real
type as before, and the alias settles the ambiguity with
Windows.UI.Composition.CompositionTarget, which is the only reason those call sites
spelled the namespace out. They lose the prefix and a comment explaining it, so the
net change is shorter code than before the port.
That also carries the fix to everything else that draws per frame -
CompositionVSync, VisualUtilities, PremiumProgressBar, PremiumSlider,
GiftCraftPopup - which until now was running on the first view's thread in any
secondary window and getting away with it only because Composition objects are
agile.
Rendered goes through the ABI too, via ICompositionTargetStatics3. Forwarding it to
the projection, as it was a commit ago, left one honest half and one broken half in
a type that exists to fix exactly that.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
#unigram
Carrying both parsers costs compile time, not binary size
Three x64 bundles built the same way: Reader 77,228,975 bytes, Pointer
77,754,662, Both 77,749,472. Both and Pointer are within 5 KB, because ILC
strips the set nothing calls. The 526 KB between Reader and Pointer is the
pointer parsers being larger native code, the same 40% the line counts show.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
UnigramDev/Unigram/develop • 85ee4c2 • 3 files, +131/-19
Register CompositionTarget.Rendering per view
Verified by Fela: video and lottie both work in the call window again.
CsWinRT keeps one event source per statics object for the whole process, so a
second view's CompositionTarget.Rendering += never reaches add_Rendering - the
delegate is appended to the registration the first view made, and the handler runs
on that view's thread. Every secondary view's frame callback lands on the main
view's thread. Composition objects do not mind, being agile, which is why the call
blobs animated throughout; WriteableBitmap does mind, and DrawFrame died on
RPC_E_WRONG_THREAD.
CompositionTargetRendering registers through the ABI instead, so each view gets a
registration of its own. The statics object is a process-wide agile singleton -
measured, along with its IAgileObject - so the call runs on the calling thread and
registers there. Proved in a throwaway app first: in the same run, the projected
subscription fired on view 1's thread and the ABI one on view 2's.
Reported as https://github.com/microsoft/CsWinRT/issues/2524.
Also drops the marshalling branch added to RegisterRendering earlier, which was
aimed at the wrong cause and never once fired; the presenter's own _loader stays.
Left alone, and noted in the todo: CompositionVSync, VisualUtilities,
PremiumProgressBar, PremiumSlider and GiftCraftPopup still subscribe through the
projection. Same latent bug, invisible so far because they only touch Composition.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
UnigramDev/Unigram/develop • 388e015 • 8 files, +114/-66
Alias CompositionTarget rather than #if every call site
Fela's idea, and a better one: CsWinRT.cs already keeps the global usings, so the
stand-in goes in there and the twenty call sites stay written the way they were.
#if NET9_0_OR_GREATER
global using CompositionTarget = Telegram.Common.CompositionTargetImpl;
#else
global using CompositionTarget = Windows.UI.Xaml.Media.CompositionTarget;
#endif
Aliasing in both directions pays for itself twice: .NET Native binds to the real
type as before, and the alias settles the ambiguity with
Windows.UI.Composition.CompositionTarget, which is the only reason those call sites
spelled the namespace out. They lose the prefix and a comment explaining it, so the
net change is shorter code than before the port.
That also carries the fix to everything else that draws per frame -
CompositionVSync, VisualUtilities, PremiumProgressBar, PremiumSlider,
GiftCraftPopup - which until now was running on the first view's thread in any
secondary window and getting away with it only because Composition objects are
agile.
Rendered goes through the ABI too, via ICompositionTargetStatics3. Forwarding it to
the projection, as it was a commit ago, left one honest half and one broken half in
a type that exists to fix exactly that.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
#unigram