todo-app/lib/frame_stats_io.dart
Krzysztof kuhy Rudnicki a874e09b65 Cut the llm-design-spec template from 12 sections to 7
Audited the template against 514 real Claude sessions (1503 top-level
prompts) joined to the 49 notes in BACKLOG.md. Sections were judged on
whether the agent actually read them, not on fill rate — four of them
said "leave blank if none" in their own helper, so a low fill rate was
the section working correctly.

Measured over the 22 note/session pairs:
  estimate  filled 22, referenced  1 (4%)
  done      filled 22, quoted back 4 (18%)
  refs      filled 14, URLs fetched 5 (35%)
  tech      the agent opened pubspec.yaml/CLAUDE.md anyway in 54%
  ask       100% ask rate with it, 92% without — no measurable effect

Kept: title, what, where, must, done. Dropped: tech (into `where`),
ask, nice and never (into `must` as `optional:` / `must not:` lines),
depends, estimate. Renamed `refs` -> `read first`. Added `verify`, the
only addition: "test it yourself, on the phone" was the single most
repeated correction in the corpus, and 3 of the 7 filled `never`
sections were already smuggling it in.

Fixes a latent data-loss bug found while testing this. parse() treats
an unknown `## heading` as content on purpose (values may contain
subheadings), so a legacy `## tech` block was folded into the preceding
section's value AND the note still reported conforming — opening any
old note in the stepper would have swallowed it on the next save.
NoteTemplate.retiredLabels now makes those notes explicitly
non-conforming, so they open raw and untouched.

tool/migrate_backlog.dart converts an old export (export -> migrate ->
import). It is idempotent and gated: it throws if a rewritten note does
not conform or if any source line vanishes. All 49 notes migrated on
the desktop and merged to the phone (0 new, 25 updated, 24 unchanged).

Also formats the few files dart format had never been run over, and
extracts a local in frame_stats_io.dart so the reflowed line stays
under 80 chars.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-26 22:03:35 +02:00

85 lines
3.0 KiB
Dart

// coverage:ignore-file
// TEMPORARY diagnostic instrument (desktop interaction-lag investigation).
// Guarded by TODO_FRAME_STATS=1 so it is inert in normal use.
//
// v3 adds an event-loop blocking monitor, which is the decisive instrument.
// Frame gaps are ambiguous in a mostly-static UI: no frame is produced when
// nothing needs drawing, so an idle user looks identical to a stalled isolate.
// A periodic timer, by contrast, is scheduled regardless of drawing -- if it
// fires late, the UI isolate was genuinely blocked, and by exactly how long.
import 'dart:async';
import 'dart:io';
import 'package:flutter/scheduler.dart';
/// Whether the frame-stats instrument is armed for this process.
bool get frameStatsEnabled => Platform.environment['TODO_FRAME_STATS'] == '1';
/// A frame whose own work exceeded this is a hitch worth naming. Set to zero
/// by TODO_FRAME_STATS_ALL=1 so every frame is logged, which is what the
/// raster-cost-vs-window-size sweep needs.
int get _slowFrameUs =>
Platform.environment['TODO_FRAME_STATS_ALL'] == '1' ? -1 : 8000;
/// How often the event-loop probe is scheduled.
const _probePeriod = Duration(milliseconds: 16);
/// Lateness beyond this means the isolate was blocked long enough to be felt.
const _blockedMs = 24;
final Stopwatch _clock = Stopwatch();
Timer? _probe;
String get _at => (_clock.elapsedMilliseconds / 1000).toStringAsFixed(2);
/// Installs the frame-timing and event-loop instruments.
void installFrameStats() {
if (!frameStatsEnabled) return;
_clock.start();
stdout.writeln(
'[frame-stats v3] armed; logging slow frames and UI-isolate blocking',
);
_installFrameTimings();
_installEventLoopProbe();
}
void _installFrameTimings() {
SchedulerBinding.instance.addTimingsCallback((timings) {
for (final t in timings) {
final build = t.buildDuration.inMicroseconds;
final raster = t.rasterDuration.inMicroseconds;
if (build + raster > _slowFrameUs) {
final total = ((build + raster) / 1000).toStringAsFixed(1);
stdout.writeln(
'[FRAME $_at s] total=${total}ms '
'(build=${(build / 1000).toStringAsFixed(1)} '
'raster=${(raster / 1000).toStringAsFixed(1)})',
);
}
}
});
}
/// Schedules a timer every [_probePeriod] and reports how late it actually
/// fired. Lateness is time the UI isolate spent unable to run anything -- e.g.
/// a synchronous JSON serialize of the whole note log.
void _installEventLoopProbe() {
var expected = _clock.elapsedMicroseconds + _probePeriod.inMicroseconds;
void tick(Timer _) {
final now = _clock.elapsedMicroseconds;
final lateMs = (now - expected) / 1000;
if (lateMs > _blockedMs) {
stdout.writeln(
'[UI-BLOCKED $_at s] event loop stalled '
'${lateMs.toStringAsFixed(1)}ms',
);
}
expected = now + _probePeriod.inMicroseconds;
}
_probe = Timer.periodic(_probePeriod, tick);
}
/// Stops the probe. Only needed if the instrument outlives the app.
void disposeFrameStats() => _probe?.cancel();