mirror of
https://github.com/block/buzz.git
synced 2026-11-04 16:31:38 +00:00
Pinky is opening this PR on Wes’s behalf. ## Summary Reduce two separately measured mobile delays without changing the relay API or removing rich message rendering: - Publish the finite channel-list snapshot without waiting for live subscription setup. - Batch active channel-list subscriptions into sorted, deterministic chunks of at most 128 explicit channel IDs, retaining unchanged chunks. - Install replacement chunks before retiring old coverage. Retain old chunks across thrown replacement failures; filter callbacks to the current relay/identity and still-desired channels; clean up retired/in-flight work across disconnect and disposal. - Scope the custom-emoji Markdown matcher to known shortcodes actually referenced in the rendered content, rather than embedding the whole community palette in every message’s regex. Preserve unknown literals, shared-colon token boundaries, event-tag URL priority, content edits, and code literals. - Honor explicit zero retry hints without inventing a ten-second session-wide gate, while preserving the ordinary live-subscription retry backoff and any already-active gate. ## Matched performance results Medians of three before and three after process-cold launches, alternated on the same authenticated iPhone 17 Pro / iOS 26.5 simulator. Before is mobile source at `e76c81968b65b0755b83efdd59dc3375c59ddf40`; after is this production patch before two documentation-only comment fixes. First channel-list frame: 11.617s → 3.179s · 73% lower latency Live setup duration: 8.475s → 0.185s · 98% lower latency Channel-open first message-list frame: 2.754s → 1.230s · 55% lower latency Message data ready → first frame: 1.977s → 0.286s · 86% lower latency Channel-open reveal complete: 2.845s → 1.394s · 51% lower latency Channel-open data readiness: 0.770s → 0.944s · 23% higher latency The gain is client-side orchestration/rendering, not a claim that the relay became faster. First channel-list frame ranges were 10.835–11.788s before and 2.872–3.395s after; channel-open first-frame ranges were 1.560–2.906s before and 1.149–1.317s after. ### Measurement boundaries - Debug simulator builds, CPU sampling disabled, bounded timestamp probes enabled identically. These are not release/physical-device measurements. - Startup clock starts at Dart `main`; build/install/native pre-main time is excluded. Auth/preferences and OS/disk caches are retained between new processes. - Same account scale: 113 active channels. Latest-message events varied slightly with live activity (1543–1546). - Channel-open uses the same initial 50-row history window, 97 query events, and 67 provider events. The 2306-entry emoji palette is explicitly loaded before navigation on both sides; palette preparation is excluded from the channel-open clock and happens after the startup frame measurement. - Both diagnostic builds temporarily disabled unused avatar segmentation to work around the existing Google ML Kit arm64-simulator slice limitation. The workaround, dependency/native changes, auto-navigation, and all probes are excluded from this PR. ## Validation - Full mobile package suite: `flutter test` — 1890 passed. - `just mobile-check` — 506 files unchanged; analyzer clean. - `just file-size-check` — policy tests and all client ratchets passed. - `git diff --check` — passed. - New lifecycle regressions cover front-sorting insertion across a chunk boundary while replacement readiness is paused, failure retention/departed-channel filtering, retired generation + disconnect cleanup, disposal, chunk limits, unchanged-set reuse, and scope switches. - Emoji unit/widget coverage includes a 2500-unused-emoji palette, unknown tokens, case matching at the component level, shared-colon boundaries, rich text, event URL priority, and content edits. - Fresh-frame source review traced the subscription queue/fences, callback scopes, duplicate-event paths, matcher/wiring, and retry scheduling. - At committed/pushed head `13a83b628c8411c5885e6f76a250ba87accf6067`, all normal pre-push hooks passed: `mobile-checks` (formatter, analyzer, and the full 1890-test mobile suite), `file-size-check`, `branch-skew`, and `push-head-scope`. The commit hook formatted 506 files with no changes. Runtime measurements preceded only the two documentation-comment fixes; no runtime source changed afterward. ## Limits / follow-ups - `RelaySession.subscribe` still settles under its existing EOSE/fallback/retryable-CLOSED contract. “Setup completed” is not an unconditional EOSE or live-delivery guarantee. This PR does not add status-aware replacement ownership. - The channel-message provider still awaits subscribe before fetching history; that separate serialization is not removed here. - Oversized Huddle queries and the separate history batching path above 128 active channels remain follow-ups, as do pre-existing read-state initialization/size warnings. - Palette-only widget refresh and upstream Markdown uppercase-dispatch behavior are not changed. - A clean source build still has the existing Google ML Kit arm64-simulator issue; the profiling workaround is not a proposed product fix. Originating Buzz conversation: buzz://message?channel=793b0522-7995-4375-b1a6-fd94a96fa21d&id=6ba88afdec78ab2cfb6728afcd4a6d10f29e6aa33ff0f62f45d6750381e4d789 --------- Signed-off-by: Wes <wesbillman@users.noreply.github.com> Co-authored-by: Pinky <5f5ab050ec58ae208332edd544ebf705221e24c1b86d82a6ca07038a7a8f6ac9@buzz.block.builderlab.xyz> Co-authored-by: Brain <1a02c72794dcd0f07058a353bc3a81f4028b8c77c92c87fce6d5c8b85970a20b@buzz.block.builderlab.xyz> Co-authored-by: Carl <9d00794d3df50972eb8b615511783cab12a77a8fd5dd5edd58073ec73b54bd8b@buzz.block.builderlab.xyz>
172 lines
4.3 KiB
Dart
172 lines
4.3 KiB
Dart
import 'dart:async';
|
|
|
|
import 'package:buzz/shared/relay/relay.dart';
|
|
import 'package:flutter_test/flutter_test.dart';
|
|
|
|
void main() {
|
|
test('uses the default only for an absent hint', () {
|
|
final timers = <_ManualTimer>[];
|
|
final gate = RelayRateLimitGate(
|
|
now: () => DateTime.utc(2026),
|
|
timerFactory: (duration, callback) {
|
|
final timer = _ManualTimer(duration, callback);
|
|
timers.add(timer);
|
|
return timer;
|
|
},
|
|
);
|
|
|
|
gate.activate(null);
|
|
|
|
expect(gate.isActive, isTrue);
|
|
expect(gate.remainingMs(), 10000);
|
|
expect(timers.single.duration, const Duration(seconds: 10));
|
|
});
|
|
|
|
test('an explicit non-positive hint opens no window', () async {
|
|
final timers = <_ManualTimer>[];
|
|
final gate = RelayRateLimitGate(
|
|
now: () => DateTime.utc(2026),
|
|
timerFactory: (duration, callback) {
|
|
final timer = _ManualTimer(duration, callback);
|
|
timers.add(timer);
|
|
return timer;
|
|
},
|
|
);
|
|
|
|
// The relay sends `retry in 0s` to mean "retry immediately".
|
|
gate.activate(0);
|
|
|
|
expect(gate.isActive, isFalse);
|
|
expect(gate.remainingMs(), 0);
|
|
expect(timers, isEmpty);
|
|
await gate.wait().timeout(const Duration(seconds: 1));
|
|
|
|
gate.activate(-5);
|
|
|
|
expect(gate.isActive, isFalse);
|
|
expect(gate.remainingMs(), 0);
|
|
expect(timers, isEmpty);
|
|
await gate.wait().timeout(const Duration(seconds: 1));
|
|
});
|
|
|
|
test('an immediate hint mid-window leaves the window intact', () async {
|
|
var now = DateTime.utc(2026);
|
|
final timers = <_ManualTimer>[];
|
|
final gate = RelayRateLimitGate(
|
|
now: () => now,
|
|
timerFactory: (duration, callback) {
|
|
final timer = _ManualTimer(duration, callback);
|
|
timers.add(timer);
|
|
return timer;
|
|
},
|
|
);
|
|
|
|
gate.activate(30);
|
|
final wait = gate.wait();
|
|
final armedTimer = timers.single;
|
|
|
|
now = now.add(const Duration(seconds: 5));
|
|
gate.activate(0);
|
|
|
|
expect(gate.isActive, isTrue);
|
|
expect(gate.remainingMs(), 25000);
|
|
expect(timers, hasLength(1));
|
|
expect(armedTimer.isActive, isTrue);
|
|
|
|
armedTimer.fire();
|
|
await wait;
|
|
expect(gate.isActive, isFalse);
|
|
});
|
|
|
|
test('clamps large hints to five minutes', () {
|
|
final timers = <_ManualTimer>[];
|
|
final gate = RelayRateLimitGate(
|
|
now: () => DateTime.utc(2026),
|
|
timerFactory: (duration, callback) {
|
|
final timer = _ManualTimer(duration, callback);
|
|
timers.add(timer);
|
|
return timer;
|
|
},
|
|
);
|
|
|
|
gate.activate(999999);
|
|
|
|
expect(timers.single.duration, const Duration(seconds: 300));
|
|
expect(gate.remainingMs(), 300000);
|
|
});
|
|
|
|
test('overlapping activations only extend the active window', () async {
|
|
var now = DateTime.utc(2026);
|
|
final timers = <_ManualTimer>[];
|
|
final gate = RelayRateLimitGate(
|
|
now: () => now,
|
|
timerFactory: (duration, callback) {
|
|
final timer = _ManualTimer(duration, callback);
|
|
timers.add(timer);
|
|
return timer;
|
|
},
|
|
);
|
|
|
|
gate.activate(30);
|
|
final wait = gate.wait();
|
|
final firstTimer = timers.single;
|
|
|
|
now = now.add(const Duration(seconds: 5));
|
|
gate.activate(10);
|
|
expect(timers, hasLength(1));
|
|
expect(gate.remainingMs(), 25000);
|
|
|
|
gate.activate(40);
|
|
expect(timers, hasLength(2));
|
|
expect(firstTimer.isActive, isFalse);
|
|
expect(gate.remainingMs(), 40000);
|
|
|
|
timers.last.fire();
|
|
await wait;
|
|
expect(gate.isActive, isFalse);
|
|
expect(gate.remainingMs(), 0);
|
|
});
|
|
|
|
test('reset releases waiters and cancels the active timer', () async {
|
|
final timers = <_ManualTimer>[];
|
|
final gate = RelayRateLimitGate(
|
|
timerFactory: (duration, callback) {
|
|
final timer = _ManualTimer(duration, callback);
|
|
timers.add(timer);
|
|
return timer;
|
|
},
|
|
);
|
|
|
|
gate.activate(20);
|
|
final wait = gate.wait();
|
|
gate.reset();
|
|
|
|
await wait;
|
|
expect(timers.single.isActive, isFalse);
|
|
expect(gate.isActive, isFalse);
|
|
});
|
|
}
|
|
|
|
class _ManualTimer implements Timer {
|
|
_ManualTimer(this.duration, this._callback);
|
|
|
|
final Duration duration;
|
|
final void Function() _callback;
|
|
bool _active = true;
|
|
|
|
void fire() {
|
|
if (!_active) return;
|
|
_active = false;
|
|
_callback();
|
|
}
|
|
|
|
@override
|
|
void cancel() => _active = false;
|
|
|
|
@override
|
|
bool get isActive => _active;
|
|
|
|
@override
|
|
int get tick => _active ? 0 : 1;
|
|
}
|