[UnigramDev/Unigram/develop](https://github.com/UnigramDev/Unigram/tree/develop) • [5b731d1](https://github.com/UnigramDev/Unigram/commit/5b731d15a3f2f7efcc4ed2d394bab74d1b2c6a86) • _1 files, +48/-17_
Reverse the ConcurrentDictionary recommendation in the review

The review ranked swapping ReaderWriterDictionary for ConcurrentDictionary as
the largest measurable win in ClientService. That was reasoning about the cost
of one lookup without ever asking how many lookups happen, and counting them
turns the recommendation around.

A full ChatCell refresh reaches a ReaderWriterDictionary eight to twelve times.
The rate of those refreshes is bounded by UI control realization — a 64px row
under a hard flick, plus aggregator refreshes of the dozen visible cells — which
puts the whole app somewhere around 0.5k to 6k lookups a second. At the ~30ns a
lock-free read would save, that is under two tenths of a millisecond per second,
or a few hundredths of one percent of a core. Wrong by a factor of a hundred it
still would not reach two percent, and at one operation per 200 microseconds the
cross-core contention that motivated the idea never happens either.

ConcurrentDictionary would cost a node allocation per entry to buy that back,
which is the wrong side of the trade in this repo. The custom class stays.

What survives is that ReaderWriterDictionary.Find allocates a closure and a LINQ
enumerator on every call to do what a foreach does for free, which is worth
fixing on its own and needs no type change.

Two things the count corrected on the way: GetChatActions is already a
ConcurrentDictionary, and GetChatFolders uses a plain Dictionary under its own
lock. Neither was ever in scope.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

[UnigramDev/Unigram/develop](https://github.com/UnigramDev/Unigram/tree/develop) • [4efdfba](https://github.com/UnigramDev/Unigram/commit/4efdfbaf7a9fe50289f526e86457e8a133ec9094) • _11 files, +85/-130_
Pass call payloads as arrays instead of IVector<byte>

Every byte crossing this boundary ends up base64 encoded into TDLib JSON, so an
IVector was the wrong shape at both ends: native built a COM collection, the
projection built an RCW, and the managed side called ToArray to get back the
byte[] the generated TDLib request wanted anyway.

UInt8[] instead. The projection copies once in each direction and managed hands
the array straight to TDLib. On the E2E path, which runs per frame, that removes
two COM objects, two RCWs and one of three copies.

Signaling stops being an event. It only ever had one listener, so a delegate
does the same job without an args runtime class per packet, and
SignalingDataEmittedEventArgs goes away entirely.

The frame transform also stops waiting on a shared semaphore. tgcalls builds one
frame transformer per simulcast layer and one per incoming channel, each on its
own thread, so several transforms run at once as soon as a call has two people
in it — a second Release before the first Wait would have thrown
SemaphoreFullException, and a waiter could be woken by another frame is answer.
Holding m_lock across the delegate used to serialise them and hide this; it
stopped doing that when that lock came off the callback path. Each transform now
waits on its own signal, with a timeout so a silent TDLib drops the frame rather
than wedging a media thread.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

[#unigram](?q=%23unigram)