move daemon services to backend isolate

The merged UI/platform thread on macOS (Flutter 3.41) freezes on any
synchronous work — file I/O, Process.run, even isolate spawning during
early frames. No timing workaround (Timer, addPostFrameCallback,
Future.delayed) was reliable.

Fix: spawn a backend isolate that owns the DaemonDispatcher, GitClient,
PqlClient, FilesService, EditorRegistry, and Toolchain resolution. The
main isolate stays free for rendering. Communication uses SendPort with
the existing IPC message protocol (IpcRequest/IpcResponse/IpcEvent) —
zero new serialization.

New files:
  backend.dart       — spawns isolate, manages SendPort/ReceivePort
  backend_entry.dart — isolate entry point, boots all services
  isolate_client.dart — replaces InProcessClient for production

Toolchain now exposes resolveToolchainPaths() as a top-level function
with self-contained PATH expansion (no module-level state that would
prevent isolate message passing).

ClideTestApp gains boot-sequence tests: compute(), Isolate.run(),
sequential Process.run, and the full resolve+exec chain.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Jeroen Schweitzer
2026-04-26 13:53:33 +02:00
co-authored by Claude Opus 4.6
parent 9fff157c9e
commit a89910dc36
9 changed files with 543 additions and 171 deletions
+51
View File
@@ -14,7 +14,9 @@ library;
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'dart:isolate' show Isolate;
import 'package:flutter/foundation.dart' show compute;
import 'package:flutter/services.dart' show rootBundle;
import 'package:flutter/widgets.dart';
@@ -134,6 +136,44 @@ class _ClideTestAppState extends State<ClideTestApp> {
_log('gitEnv', '${tc.gitEnv}');
print('[testmode]');
// Boot sequence simulation tests
print('[testmode] --- boot sequence ---');
await _testAsync('compute(resolveToolchainPaths)', () async {
final paths = await compute(resolveToolchainPaths, workDir);
return 'git=${paths.git} pql=${paths.pql}';
});
await _testAsync('Isolate.run(resolveToolchainPaths)', () async {
final paths = await Isolate.run(() => resolveToolchainPaths(workDir));
return 'git=${paths.git} pql=${paths.pql}';
});
await _testAsync('git rev-parse (project.open sim)', () async {
final r = await Process.run(tc.git, ['rev-parse', '--show-toplevel'],
workingDirectory: workDir, environment: tc.gitEnv);
return 'exit=${r.exitCode} ${(r.stdout as String).trim()}';
});
await _testAsync('sequential git calls', () async {
final r1 = await Process.run(tc.git, ['rev-parse', '--show-toplevel'],
workingDirectory: workDir, environment: tc.gitEnv);
final r2 = await Process.run(tc.git, ['rev-parse', '--abbrev-ref', 'HEAD'],
workingDirectory: workDir, environment: tc.gitEnv);
return 'root=${(r1.stdout as String).trim()} branch=${(r2.stdout as String).trim()}';
});
await _testAsync('compute + immediate Process.run', () async {
final paths = await compute(resolveToolchainPaths, workDir);
final tc2 = Toolchain();
tc2.applyResolved(paths);
final r = await Process.run(tc2.git, ['rev-parse', '--show-toplevel'],
workingDirectory: workDir, environment: tc2.gitEnv);
return 'exit=${r.exitCode} ${(r.stdout as String).trim()}';
});
print('[testmode]');
}
// -- ipc category ---------------------------------------------------------
@@ -279,6 +319,17 @@ class _ClideTestAppState extends State<ClideTestApp> {
setState(() => _results.add(r));
}
Future<void> _testAsync(String label, Future<String> Function() fn) async {
try {
final result = await fn().timeout(const Duration(seconds: 10));
_addResult(label, true, result);
} on TimeoutException {
_addResult(label, false, 'TIMEOUT (10s)');
} catch (e) {
_addResult(label, false, '$e');
}
}
Future<void> _testExec(String label, String bin, List<String> args, String workDir, {Map<String, String>? env}) async {
try {
final r = await Process.run(bin, args, workingDirectory: workDir, environment: env)