Devlog: Calendar Load Profiling Audit¶
v0.12.8.3 - Algorithm A & C Validation¶
Overview¶
This update validates the performance impact of implementing Algorithm A (Keyed Identity Diffing) and Algorithm C (Reverse-Index Mapping) to resolve the syncCalendar bottleneck.
Performance Comparison Table¶
| Metric (DailyNote Stage-2 Sync) | v0.12.8.2 (Baseline) | v0.12.8.3 (Optimized) | Delta |
|---|---|---|---|
| Total Sync Time | ~5137ms | ~1482ms | -71.1% |
| diffPrepMs | ~4942ms | ~1283ms | -74.0% |
| removeMappingMs | ~1648ms | ~0.2ms | -99.9% |
| addMappingMs | ~3292ms | ~1279ms | -61.1% |
Key Findings¶
- Algorithm C Success: The transition to a sessionId-based reverse index for removals was a total success.
removeMappingMsessentially vanished from the profile (1.6s -> 0ms). - Synchronization Gain: The overhead of diffing is now drastically lower due to Keyed Identity Diffing. However, a significant stall remains in the "addition" path.
- Remaining Bottleneck: addMapping Parsing Pressure: addMappingMs still consumes ~1.3 seconds during large syncs. New instrumentation shows this is because addMapping continues to call getEventHandle(), which triggers expensive file parsing (slow getEventsInFile) even when the identity is deterministically known via the sync key.
- UI Thread Impact: While the main thread stall is shorter, it is still long enough to trigger JXL violations (~164ms RAF handler).
Next Steps¶
- Optimize addMapping: Modify ProviderRegistry.addMapping to accept or leverage the SyncKey to avoid redundant provider calculations.
- Batching: Investigate batching or yielding during the
ewLoopto prevent the UI thread from freezing for >1s during massive additions.
v0.12.8.2 - Algorithm A & C Validation¶
- Date: 2026-04-11
- Plugin version context:
0.12.8.2 - Investigation scope: calendar open latency, staged loading behavior, and large-update responsiveness
- Primary evidence source: runtime trace log from Obsidian developer console
- Status: investigation complete; profiling instrumentation added and validated in follow-up run
Executive Summary¶
The strongest bottleneck is not initial stage-1 fetching and not the first blank-to-calendar render.
The largest single cost in the captured trace is EventCache.syncCalendar() during the large Daily Note stage-2 update from 716 -> 1413 Daily Note events:
- Daily Note provider full fetch (
268files,1413events): about321ms - EventCache large sync (
previousCount=716,incomingCount=1413): about4106ms - Dominant substep inside EventCache sync (
removeCount=716,addCount=1413):diffPrepMsabout3916ms - Subsequent UI refresh after that sync (
totalVisibleEvents=1413): about351ms
Important context: a smaller but still material Daily Note stage-1 synchronization also occurs earlier (0 -> 716 events), with EventCache.syncCalendar() at about 1464ms (diffPrepMs about 1347ms).
By comparison, the initial blank-to-calendar path is materially smaller:
- Total
setViewState()completion: about151ms - First animation frame after FullCalendar render: about
153ms - Initial FullCalendar render: about
69-71ms - Local stage-1 fetch: about
3-5ms(5events)
Follow-up (same day) with deeper bottleneck logs enabled by default confirms the dominant internal culprit:
- Stage-1 Daily Note diff prep (
0 -> 716): about1680ms addMappingMs: about1679ms- Stage-2 Daily Note diff prep (
716 -> 1413): about4942ms removeMappingMs: about1648msaddMappingMs: about3292ms
This isolates the hotspot to provider-registry mapping operations during cache diff prep, not provider fetch and not initial render.
Key Findings¶
- The initial blank page is real, but it is not the dominant performance problem.
- The biggest bottleneck is cache synchronization and identifier/mapping preparation for large Daily Note payloads (especially the stage-2
716 -> 1413merge path). - The next most expensive area is post-sync UI rebuild work:
ViewEnhancer.getEnhancedData()plus FullCalendar source replacement. - Provider fetch/parsing for Daily Notes is meaningful (
251msfor range-limited stage-1,321msfor full stage-2), but still much smaller than cache sync costs (1464msand4106ms). - Small remote calendars and local stage-1 work are not performance-critical in this trace.
- Follow-up deep profiling shows
addMapping(...)andremoveMapping(...)account for almost alldiffPrepMson large Daily Note syncs.
Instrumentation Coverage¶
This investigation added profiling logs to the following runtime areas:
src/main.tssrc/ui/view.tssrc/ui/settings/sections/calendars/calendar.tssrc/core/EventCache.tssrc/core/ViewEnhancer.tssrc/providers/ProviderRegistry.tssrc/providers/dailynote/DailyNoteProvider.ts
The logs capture:
- view activation and
setViewState()timing onOpen()progressionloadSettings()timing- stage-1 and stage-2 provider timings
- Daily Note range filtering and per-file parse timing
- cache enhancement, compare, diff preparation, store writes, and flush timing
ViewEnhancer.getEnhancedData()timing- FullCalendar module load, calendar construction, and
cal.render()timing - post-update source removal/addition and next-frame timing
Follow-up instrumentation also captures:
syncCalendardiff-prep internal breakdown (oldLoopMs,newLoopMs,removeMappingMs,addMappingMs,generateIdMs)- partial-update UI source work split (
sourceLookupMs,sourceAddMs, found/missing counts)
Profiling Results¶
0. Follow-Up Deep-Profiling Run (After Instrumentation)¶
This section summarizes the newer trace captured after enabling detailed bottleneck logs by default.
Initial-open path in this run is slower than the baseline, but still not the dominant issue:
setViewState()completion: about266ms- first frame after FullCalendar render: about
270ms
Daily Note provider work in this run:
- stage-1 provider (
101files,716events):totalMsabout390ms - stage-2 provider (
268files,1413events):totalMsabout438ms
Daily Note cache sync in this run:
- stage-1 sync (
0 -> 716):totalMsabout1811ms,diffPrepMsabout1680ms - stage-2 sync (
716 -> 1413):totalMsabout5137ms,diffPrepMsabout4942ms
Diff-prep substep attribution (new evidence):
- stage-1 (
0 -> 716): newLoopMs:1680.2msaddMappingMs:1679.3msgenerateIdMs:0.3ms- stage-2 (
716 -> 1413): oldLoopMs:1648.5msremoveMappingMs:1648.2msnewLoopMs:3293.8msaddMappingMs:3291.8msgenerateIdMs:0.2ms
Interpretation:
addMapping(...)andremoveMapping(...)dominatediffPrepMsalmost entirely.generateId()is negligible.- provider fetch remains materially smaller than cache sync.
UI refresh after large stage-2 sync in this run:
ViewEnhancer.getEnhancedData(): about187.6ms- source removal: about
4.8ms - source addition: about
159ms - total view update work: about
355.4ms - next frame after update: about
361.8ms - detailed UI split:
sourceLookupMs: about0mssourceAddMs: about159ms
Interpretation:
- UI remains a secondary bottleneck.
- For this path, source lookup is negligible; source add and enhancement dominate UI update cost.
1. Initial Open Path¶
Observed opening sequence:
activateView()started traceonOpen()entered at about13msloadSettings()finished at about22mscache.populate()finished at about71ms- shell DOM created at about
72ms renderCalendar()called at about73ms- FullCalendar
cal.render()completed at about69.4msinside render step setViewState()finished at about151ms- first post-render animation frame landed at about
153ms
Interpretation:
- The initial blank-to-not-blank path is not dominated by provider I/O.
- The biggest visible cost during initial open is FullCalendar's first render.
2. Stage-1 Provider Costs¶
Local stage-1:
local_1fetch:3.3ms(5events)- local stage-1 complete:
5.1ms
Remote stage-1 small calendars:
ical_1:1event in about122msical_2:4events in about129msical_3:1event in about131msdailynote_3stage-1 (range-limited):716events in about252msprovider time
Interpretation:
- Small remote providers are not causing the multi-second stall seen later.
- Local stage-1 fetch is fast and not a bottleneck.
- Daily Note stage-1 provider work is moderate, but the dominant stage-1 cost is the subsequent EventCache sync.
3. Daily Note Provider Costs¶
Range-limited stage-1 setup:
- Daily Note range filter:
268files down to101files in2.6ms
Stage-1 (range-limited) Daily Note fetch:
filesProcessed:101totalEvents:716fetchMs:241.3mstotalMs:251.4ms
Full stage-2 Daily Note fetch:
filesProcessed:268totalEvents:1413fetchMs:318.7mstotalMs:321ms
Per-file trace pattern:
- many files show total times roughly
120msto310ms - most of that time is reported under
parseMs - metadata wait is much smaller, generally about
4msto9ms
Interpretation:
- Daily Note parsing is non-trivial and clearly worth attention.
- However, both stage-1 and stage-2 provider fetches remain far smaller than the cache synchronization costs that follow.
4. EventCache Costs¶
Small calendars:
local_1stage-1 sync: about1msical_1sync: about1.5msical_2sync: about0.5msical_3sync: about0.4ms
Daily Note stage-1 sync:
incomingCount:716previousCount:0enhanceMs:2.7msdiffPrepMs:1347msstoreMs:0.6msflushMs:113.5mstotalMs:1464ms
Large Daily Note stage-2 sync:
incomingCount:1413previousCount:716enhanceMs:5.1msdiffPrepMs:3915.6msstoreMs:1msflushMs:183.8mstotalMs:4105.8ms
Interpretation:
- The dominant bottleneck in the full trace is
diffPrepMs. - Event enhancement itself is cheap.
- Store writes are cheap.
- The expensive work is in preparing removals/additions and related identifier/mapping work before store commit.
- This pattern holds at both Daily Note stages, but the stage-2 (
716 -> 1413) merge path is substantially worse.
5. View and Render Costs After Large Sync¶
For the large Daily Note update:
- view received update at about
6012ms ViewEnhancer.getEnhancedData():183ms- source removal:
4.8ms - source addition:
159ms - total view update render work:
350.7ms - next animation frame after update:
359.2ms
Interpretation:
- Post-sync rendering is expensive, but still much smaller than the cache sync bottleneck.
- The two main costs here are:
ViewEnhancer.getEnhancedData()- FullCalendar source re-addition
Additional runtime signal:
- browser violation logged:
'requestAnimationFrame' handler took 164ms - browser violation logged:
'wheel' input event was delayed for 2488 ms due to main thread being busy
Bottleneck Ranking¶
Based on the latest deep-profiled trace, the main bottlenecks rank as follows:
EventCache.syncCalendar()stage-2diffPrepMs(716 -> 1413): about4942msaddMapping(...)inside stage-2 diff prep: about3292msremoveMapping(...)inside stage-2 diff prep: about1648ms- Full
EventCache.syncCalendar()stage-2 total: about5137ms - Full
EventCache.syncCalendar()stage-1 total: about1811ms EventCache.syncCalendar()stage-1diffPrepMs: about1680ms- View refresh after large stage-2 sync: about
355ms - Daily Note provider full fetch and parse (stage-2): about
438ms - Daily Note provider range-limited fetch and parse (stage-1): about
390ms
Current Conclusion¶
The biggest bottleneck is the cache synchronization layer, specifically provider-registry mapping operations (addMapping(...) and removeMapping(...)) inside EventCache.syncCalendar() diff preparation for large Daily Note payload merges.
The current evidence does not support the idea that "fetching a couple of events" is the main problem. In this trace, the high-cost path is large-volume cache prep and subsequent view rebuild work after the Daily Note provider returns a much bigger payload.
More specifically:
- provider fetch for
1413Daily Note events is about438msin the latest run - cache sync for the same update is about
5137ms - the dominant share of that sync is
diffPrepMsat about4942ms - inside that
diffPrepMs,addMappingMs(~3292ms) +removeMappingMs(~1648ms) explain essentially all of the cost
Suggested Next Profiling Pass¶
diffPrepMs has now been split and the dominant substeps are known. The next profiling pass should go one level deeper inside provider-registry mapping:
getGlobalIdentifier(...)internals- provider
getEventHandle(...)internals - map/set operations and key construction in mapping structures
- object allocation and string-generation pressure along mapping paths
It would also be useful to refine UI profiling for the large update path:
addEventSource(...)cost per source (confirmed meaningful)- event mount count and
eventDidMountpressure
sources.find(...) cost is currently negligible in this path, but should still be monitored for higher source-count configurations.
Decision Record¶
- Profiling instrumentation was added during this investigation to isolate diff-prep and UI substep costs.
- No optimization was applied yet; this document records measurement evidence only.
- The purpose of this dev log is to preserve evidence before implementation work begins.
- This document has been reconciled against the raw trace to ensure timing and event-count accuracy for both Daily Note stage-1 and stage-2 paths.