Throttle Dashboard live metrics: stop per-second ReadList spam
Details
The 1-second live metrics timer was calling GetDataMetricsAsync on every
tick regardless of active tab. This triggered ~33 SDK ReadList/Count
calls per second across all plugins (page, task, event, contact, etc.),
saturating the SDK message bus with unnecessary I/O.
- Memory metrics (process-level, no SDK calls) still refresh every 1s
- Data metrics now only run on the Data tab, throttled to every 10s
- Overview tab DataStorageTotal populated on first load only
- Subsystems tab refresh unchanged (local counters, no SDK calls)
Fix duckdb_tables query error after SQLCipher migration
Details
DataPlugin.GetMetricsAsync was querying duckdb_tables() which no longer
exists after the DuckDB → SQLCipher migration. Replaced with
sqlite_master query to find dataset tables (ds_* pattern).
SQLite doesn't expose per-table byte sizes like DuckDB did, so the
method now returns a set of existing table names and the caller uses
the existing row-count × column-count heuristic for all tables.
- Rename QueryDuckDbTableSizesAsync → QueryDatasetTablesAsync
- Return HashSet<string> instead of Dictionary<string, long>
- Query sqlite_master WHERE type='table' AND name LIKE 'ds_%'
- Fix stale DuckDB references in comments
- Bump Data plugin 1.21.1 → 1.21.2
Fix Dashboard plugin toggle not rendering + clean up alloc tracking
Details
The ToggleSwitch for enabling/disabling plugins was invisible because
ToggleSwitch in Avalonia does not have Command/CommandParameter properties.
The XAML parser silently dropped the invalid attributes, preventing the
control from rendering.
- Remove invalid Command/CommandParameter from ToggleSwitch
- Switch to two-way IsChecked binding on IsActivated property
- Add OnActivationChanged callback on DashboardPluginItem that fires
when IsActivated changes from the ToggleSwitch
- Wire callback in PopulateLocalPlugins AFTER setting initial value
to avoid re-entrant enable/disable calls during initialization
- Add OnPluginActivationChanged handler that syncs to plugin registry
Also cleans up SubsystemTracker: removed broken heartbeat approach for
long-running task alloc tracking (GC.GetAllocatedBytesForCurrentThread
cannot be queried cross-thread after await resumes on a different thread).
Fix Subsystems tab: use process-level native memory, improve column layout
Details
The "Native Memory" summary card was showing only Rust allocator-tracked
bytes (~126 B), which is misleading when the process uses ~1.4 GB native.
The Rust GlobalAlloc only tracks Rust heap allocations — C/C++ libraries
(SQLCipher, ONNX, Whisper, LLamaSharp) use their own malloc directly.
- Summary "Native Memory" card now uses WorkingSet64 - GC heap (same
calculation as the Overview tab's memory card)
- Per-subsystem memory column renamed to "Tracked Memory" and widened
to accommodate labels like "126 B native" or "7.0 MB managed"
- Memory display now distinguishes native vs managed with explicit labels
- Column widths adjusted (Category 80, Tasks 60, Memory 180, Rate 100)
- Removed broken heartbeat approach for long-running task alloc tracking
(GC.GetAllocatedBytesForCurrentThread can't be queried cross-thread)
Add per-subsystem memory tracker and Dashboard Subsystems tab
Details
Adds visibility into which subsystems consume memory across .NET managed
heap, native Rust allocations, and background task activity. Enables
live per-subsystem metrics in the Dashboard.
Rust tracking allocator (Phase 1):
- New allocator.rs: custom GlobalAlloc with 16-byte header per allocation
storing size + subsystem ID. Per-subsystem atomic counters track bytes
and allocation counts. Thread-local subsystem tag set via with_subsystem()
scope guard.
- Subsystem IDs: untagged(0), storage(1), sync(2), crypto(3), cloud(4),
plugins(5), ffi(6), reserved(7)
- New FFI export: privstack_subsystem_memory() returns JSON snapshots
- Key entry points wrapped: privstack_execute (FFI), auth_initialize/unlock
(Crypto), sync_status (Sync)
C# SubsystemTracker (Phase 2):
- SubsystemTracker: singleton service with AsyncLocal tagging, RunTagged()
for async/sync task wrapping, EnterScope() for synchronous code blocks,
5-sample rolling allocation rate, native counter merging from Rust FFI
- SubsystemDefinitions: static table of 16 built-in subsystems across
UI, AI, Core, Services, and Runtime categories
- Static RunTaggedStatic() convenience for instrumentation without DI
Entry point instrumentation (Phase 3):
- RagIndexService: consumer + deferred init tagged as ai.rag
- IntentEngine: signal consumer tagged as ai.intent
- EmbeddingService: ONNX load + tokenizer tagged as ai.embedding
- LocalLlamaProvider: model load tagged as ai.llm
- WhisperService: model load tagged as ai.whisper
- IpcServer: listener tagged as ipc
- ReminderSchedulerService: initial poll tagged as reminders
- FileEventInboundScanner: poll loop tagged as core.sync
- SyncOutboundService: debounce callback tagged as core.sync
- CloudSyncSettingsViewModel: push + status loop tagged as core.cloud
- UpdateViewModel: startup check tagged as updates
Dashboard Subsystems tab (Phase 4):
- New SubsystemItemViewModel with status dot coloring (active/idle/stopped)
- DashboardViewModel extended with Subsystems tab, summary cards (threads,
managed heap, native memory, active count), per-subsystem item collection
- DashboardView.axaml: third tab button, summary stat cards, table-style
subsystem list with status dot, name, category, task count, memory,
alloc rate columns
- Live refresh on 1-second timer when Subsystems tab is active
Registration wiring (Phase 5):
- SubsystemTracker registered as singleton in CoreServiceRegistration
- Static subsystems registered at startup, eagerly resolved in WireCorePostBuild
- Plugin subsystems registered dynamically in DashboardPlugin.CreateViewModelCore()
- NativeLibrary P/Invoke added for privstack_subsystem_memory
Version bumps:
- Rust workspace: 1.15.3 → 1.15.4
- Desktop app: 1.68.6 → 1.69.0
- Dashboard plugin: 1.4.0 → 1.5.0
Get notified about new releases