mirror of
https://github.com/kuhyx/todo-app.git
synced 2026-11-10 19:27:01 +01:00
First step of moving the desktop app off Flutter's Linux embedder, which tops
out at ~20fps at 4K (see docs/desktop-performance-findings.md). Android is
unaffected and keeps every existing code path.
The blocker was that `dart:io` was imported by note_repository, capture_screen
and settings_screen. On web it compiles to a stub and then throws at runtime --
the first web build rendered a blank white window. So each `dart:io` use now
sits behind a conditional export, with the shared logic hoisted into pure Dart:
- note_repository loses `dart:io`/sqlite entirely. Platform wiring moves to
repository_factory{,_io,_web}, and the legacy sqlite import moves to its own
io-only library. The HLC-from-updated_at seeding is preserved verbatim, so
independently-migrating devices still resolve pre-cutover edits by real edit
time.
- Notes persist to IndexedDB on web, not localStorage: the log is the primary
copy of the backlog and localStorage caps at ~5-10MB and is evicted eagerly.
- BACKLOG.md and the explicit Export keep working through a local wrapper
(added next). A browser cannot write `~/todo/BACKLOG.md`, and the user's
tooling plus the todo MCP server read that path, so leaving it to rot was not
an option. Writes are best-effort so the app still works with no wrapper.
- Notes are also mirrored to the wrapper's on-disk copy, so a wiped Chrome
profile is not a total-loss event for notes that have not reached GitHub yet.
`openRepository` is split so `openRepositoryIn` keeps the migration tests; the
suite stays at 174 green and 100% coverage.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
85 lines
3.0 KiB
Dart
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) {
|
|
stdout.writeln(
|
|
'[FRAME $_at s] total=${((build + raster) / 1000)
|
|
.toStringAsFixed(1)}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();
|