Skip to content

Commit 40ec8d3

Browse files
Gheat1hamidi-dev
authored andcommitted
perf(cache): serve records_cost from the warm cache
pi/OpenClaw/CSV/JSONL probed records_cost in their constructors by reading the whole corpus, and make_store builds the leaf before CachedStore wraps it -- so even a 100% cache hit paid a serial full read of every session file. Make records_cost a lazy property on those four backends (derived from an already-run parse, with the early-exit probe only as a first-read fallback), persist it in the cache payload (CACHE_VERSION 3), and have CachedStore answer it on a fingerprint hit without touching the leaf. CombinedStore's AND over its backends turns into a cached_property for the same reason. While at it, a cache-hit row that no longer matches the Workflow dataclass falls back to a real parse instead of crashing.
1 parent df76f0a commit 40ec8d3

7 files changed

Lines changed: 230 additions & 30 deletions

File tree

src/opentab/stores/cached.py

Lines changed: 30 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,12 @@
99
falls through to a normal parse, then rewrites the cache, so a stale rollup is never
1010
shown; mtime is nanosecond-grained, so an in-place edit reliably invalidates.
1111
12-
Only workflows() and model_breakdown() are intercepted -- they feed the first frame.
13-
Everything else (workflow_nodes, tool_breakdown, message_timeline, supports_*, summary,
14-
records_cost, demo, demo_scale, ...) delegates straight to the wrapped store, which
15-
parses lazily the first time you actually drill into a session. So a warm start paints
16-
instantly and only pays the parse if and when you open a session's detail.
12+
Only workflows(), model_breakdown() and records_cost are intercepted -- they feed the
13+
first frame (records_cost because some backends can only answer it by reading their
14+
whole corpus). Everything else (workflow_nodes, tool_breakdown, message_timeline,
15+
supports_*, summary, demo, demo_scale, ...) delegates straight to the wrapped store,
16+
which parses lazily the first time you actually drill into a session. So a warm start
17+
paints instantly and only pays the parse if and when you open a session's detail.
1718
1819
The cache is disabled under --demo (demo never persists, and its per-process scale must
1920
not be baked in) and --no-cache; sources.make_store applies the wrapper.
@@ -28,7 +29,7 @@
2829

2930
from opentab.models import Workflow
3031

31-
CACHE_VERSION = 2 # bump when the cached payload shape changes (invalidates old files)
32+
CACHE_VERSION = 3 # bump when the cached payload shape changes (invalidates old files)
3233

3334

3435
def cache_dir() -> str:
@@ -49,10 +50,21 @@ def __init__(self, store, cache_id: str, args: argparse.Namespace):
4950
self.served_from_cache: bool | None = None # set by workflows(); read by --timings
5051

5152
# Anything not intercepted below is the wrapped store's -- workflow_nodes, the Turns/
52-
# Tools extras, supports_*, records_cost, demo, source_name, summary, and so on.
53+
# Tools extras, supports_*, demo, source_name, summary, and so on.
5354
def __getattr__(self, name):
5455
return getattr(self._store, name)
5556

57+
@property
58+
def records_cost(self) -> bool:
59+
# Served from the cache on a fingerprint hit: some backends (pi/OpenClaw/CSV/
60+
# JSONL) can only answer this by reading their whole corpus, which would defeat
61+
# the warm start. A miss (or a pre-v3 cache) delegates like __getattr__ does.
62+
if self._disk is not None and "records_cost" in self._disk:
63+
fp = self._live_fp if self._live_fp is not None else self._fingerprint()
64+
if self._disk.get("fingerprint") == fp:
65+
return bool(self._disk["records_cost"])
66+
return getattr(self._store, "records_cost", True)
67+
5668
# --- fingerprint / cache file -------------------------------------------------
5769
def _fingerprint(self) -> list:
5870
# Sorted [path, size, mtime_ns] over the backend's inputs. Lists (not tuples) so
@@ -89,6 +101,9 @@ def _write(self, fingerprint: list, workflows: list, model_breakdown: list) -> N
89101
"version": CACHE_VERSION,
90102
"source": self._source,
91103
"fingerprint": fingerprint,
104+
# Cheap here: the backend just parsed, so a lazy records_cost derives from
105+
# that parse instead of running its full-corpus probe.
106+
"records_cost": bool(getattr(self._store, "records_cost", True)),
92107
"workflows": workflows,
93108
"model_breakdown": model_breakdown,
94109
}
@@ -108,9 +123,14 @@ def workflows(self) -> list:
108123
# fingerprint (the common warm start / no-op reload) serves the cache untouched.
109124
self._live_fp = self._fingerprint()
110125
if self._disk is not None and self._disk.get("fingerprint") == self._live_fp:
111-
self._fresh_wf = None # a hit: nothing new to write
112-
self.served_from_cache = True
113-
return [Workflow(**row) for row in self._disk["workflows"]]
126+
try:
127+
rows = [Workflow(**row) for row in self._disk["workflows"]]
128+
except TypeError:
129+
self._disk = None # cached fields drifted from the dataclass: reparse
130+
else:
131+
self._fresh_wf = None # a hit: nothing new to write
132+
self.served_from_cache = True
133+
return rows
114134
workflows = self._store.workflows() # miss: real parse
115135
self._fresh_wf = [asdict(w) for w in workflows]
116136
self.served_from_cache = False

src/opentab/stores/combined.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
from __future__ import annotations
33

44
from concurrent.futures import ThreadPoolExecutor
5+
from functools import cached_property
56

67
from opentab.models import Workflow
78

@@ -45,7 +46,6 @@ class CombinedStore:
4546

4647
def __init__(self, stores: list):
4748
self.stores = stores
48-
self.records_cost = all(getattr(s, "records_cost", True) for s in stores)
4949
# Combined demo: each backend would otherwise draw its own random hidden scale,
5050
# which would distort the cross-source ratio (the Sources view lies about the
5151
# OpenCode-vs-Claude proportion). Share ONE scale across all backends so the
@@ -66,6 +66,13 @@ def __init__(self, stores: list):
6666
)
6767
self._owner: dict[str, object] = {}
6868

69+
@cached_property
70+
def records_cost(self) -> bool:
71+
# AND of the backends (False when any doesn't record cost), evaluated lazily so
72+
# building the merged view never forces a backend's full-corpus cost probe --
73+
# after workflows() the warm-start cache answers this for free.
74+
return all(getattr(s, "records_cost", True) for s in self.stores)
75+
6976
def workflows(self) -> list[Workflow]:
7077
# Roll up every backend in parallel, then build the id->owner map and merge on
7178
# this thread (deterministic, order-preserving) -- see _gather.

src/opentab/stores/csv_source.py

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,9 @@ class CsvStore:
3939
list rates.
4040
But cost is handled per-row like HermesStore: if the CSV carries a cost_usd/credits
4141
column with positive values those rows price as real spend, so records_cost is a
42-
per-instance attr (True iff any row has a recorded cost), probed cheaply in __init__.
42+
per-instance property (True iff any row has a recorded cost), resolved lazily --
43+
derived from a parse when one has run, else probed on first read (never in __init__,
44+
so the warm-start cache can answer it without touching the file).
4345
4446
Models are mixed-provider, so each id is provider-prefixed (claude->anthropic/,
4547
gpt|o3->openai/, gemini->google/) for pricing and the Providers rollup. OpenAI-style
@@ -103,7 +105,7 @@ def __init__(self, csv_path: str, args: argparse.Namespace):
103105
self.demo_scale = 3.0 ** random.uniform(-1.0, 1.0) if self.demo else 1.0
104106
self._sessions: dict[str, dict] | None = None # parsed lazily / on reload
105107
self._git_root_cache: dict[str, str] = {}
106-
self.records_cost = self._probe_records_cost()
108+
self._records_cost: bool | None = None # resolved lazily (records_cost property)
107109

108110
# --- header / value parsing ---------------------------------------------
109111
@classmethod
@@ -207,9 +209,23 @@ def _resolve_dir(self, project: str) -> str:
207209
return self._git_root(project)
208210
return project
209211

212+
@property
213+
def records_cost(self) -> bool:
214+
# True iff any row records a positive cost. Lazy so construction never reads the
215+
# file (the warm-start cache answers a hit without reaching here): after a parse
216+
# it derives from the accumulated per-model costs; the full-file probe runs only
217+
# when it is read before any parse.
218+
if self._sessions is not None:
219+
return any(
220+
acc["cost"] > 0 for s in self._sessions.values() for acc in s["models"].values()
221+
)
222+
if self._records_cost is None:
223+
self._records_cost = self._probe_records_cost()
224+
return self._records_cost
225+
210226
def _probe_records_cost(self) -> bool:
211-
# True iff the CSV has a cost column with any positive value. Cheap pass so it is
212-
# safe in __init__ (CombinedStore reads records_cost before workflows()).
227+
# True iff the CSV has a cost column with any positive value. Early-exits so it
228+
# stays cheap.
213229
try:
214230
with open(self.csv_path, newline="", encoding="utf-8", errors="replace") as fh:
215231
reader = csv.DictReader(fh)

src/opentab/stores/jsonl_source.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ def __init__(self, path: str, args: argparse.Namespace):
8181
self.demo_scale = 3.0 ** random.uniform(-1.0, 1.0) if self.demo else 1.0
8282
self._sessions: dict[str, dict] | None = None
8383
self._git_root_cache: dict[str, str] = {}
84-
self.records_cost = self._probe_records_cost()
84+
self._records_cost: bool | None = None # resolved lazily (records_cost property)
8585

8686
def cache_inputs(self) -> list[str]:
8787
# The single JSONL file whose (size, mtime) fingerprints the warm-start cache.
@@ -107,8 +107,8 @@ def _row_cost(self, obj: dict) -> float:
107107
return 0.0
108108

109109
def _probe_records_cost(self) -> bool:
110-
# True iff any line records a positive cost. Cheap pass, safe in __init__
111-
# (CombinedStore reads records_cost before workflows()).
110+
# True iff any line records a positive cost. Early-exits so it stays cheap; only
111+
# run when records_cost (the lazy CsvStore property) is read before any parse.
112112
try:
113113
with open(self.path, encoding="utf-8", errors="replace") as fh:
114114
for line in fh:

src/opentab/stores/openclaw.py

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -31,9 +31,10 @@ class OpenClawStore:
3131
**unpriced** (the "$" view estimates them) while metered messages with a real cost price
3232
as spend. The two accumulate independently per message, so a session (even one model)
3333
mixing both routes is split correctly. Cost is therefore mixed, so -- exactly like
34-
`CsvStore`/`HermesStore`/`PiStore` -- **`records_cost` is a per-instance attr** (True iff
35-
any *metered* message has a cost), set by a cheap early-exit probe in `__init__` so
36-
`CombinedStore` can read it before `workflows()`.
34+
`CsvStore`/`HermesStore`/`PiStore` -- **`records_cost` is a per-instance property** (True
35+
iff any *metered* message has a cost), resolved lazily: derived from a parse when one
36+
has run, else by an early-exit probe on first read (never in `__init__`, so construction
37+
stays free and the warm-start cache can answer it without touching the files).
3738
3839
Parsing: each session file is newline-delimited JSON,
3940
and only `type:"message"` records with `message.role == "assistant"` and a `message.usage`
@@ -85,7 +86,7 @@ def __init__(self, root_dir: str, args: argparse.Namespace):
8586
# provider logs in via "oauth" (a consumer plan, subscription) or a static "token"
8687
# (a metered API key). Read-only; we read only the mode + provider name.
8788
self._oauth_providers = self._load_oauth_providers()
88-
self.records_cost = self._probe_records_cost()
89+
self._records_cost: bool | None = None # resolved lazily (records_cost property)
8990

9091
# --- mixed-provider model ids (mirrors CsvStore) -------------------------
9192
@staticmethod
@@ -276,10 +277,24 @@ def _files(self) -> list[str]:
276277
out.append(path)
277278
return out
278279

280+
@property
281+
def records_cost(self) -> bool:
282+
# True iff any *metered* (non-subscription) message records real spend. Lazy so
283+
# construction never reads the corpus (the warm-start cache answers a hit without
284+
# reaching here): after a parse it derives from the accumulated per-model costs;
285+
# the full-file probe runs only when it is read before any parse.
286+
if self._sessions is not None:
287+
return any(
288+
acc["cost"] > 0 for s in self._sessions.values() for acc in s["models"].values()
289+
)
290+
if self._records_cost is None:
291+
self._records_cost = self._probe_records_cost()
292+
return self._records_cost
293+
279294
def _probe_records_cost(self) -> bool:
280295
# True iff any *metered* (non-subscription) assistant message records a positive
281-
# cost. Early-exits so it stays cheap (safe in __init__; CombinedStore reads it
282-
# before workflows()). A subscription-only setup -> False (every cost is estimated).
296+
# cost. Early-exits so it stays cheap. A subscription-only setup -> False (every
297+
# cost is estimated).
283298
for path in self._files():
284299
try:
285300
fh = open(path, encoding="utf-8", errors="replace")

src/opentab/stores/pi.py

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -31,9 +31,10 @@ class PiStore:
3131
**unpriced** (the "$" view estimates them), while metered messages with a real cost
3232
price as spend. The two accumulate independently per message, so a session (even one
3333
model) mixing both routes is split correctly. Cost is therefore mixed, so -- exactly
34-
like `CsvStore`/`HermesStore` -- **`records_cost` is a per-instance attr** (True iff any
35-
*metered* message has a cost), set by a cheap early-exit probe in `__init__` so
36-
`CombinedStore` can read it before `workflows()`.
34+
like `CsvStore`/`HermesStore` -- **`records_cost` is a per-instance property** (True iff
35+
any *metered* message has a cost), resolved lazily: derived from a parse when one has
36+
run, else by an early-exit probe on first read (never in `__init__`, so construction
37+
stays free and the warm-start cache can answer it without touching the files).
3738
3839
Each session file is newline-delimited JSON: a `session` record carries the canonical
3940
id + **cwd** (so directories fold to the **git root**, no path-decoding the project
@@ -66,7 +67,7 @@ def __init__(self, root_dir: str, args: argparse.Namespace):
6667
# "$" view estimates them -- exactly like HermesStore's billing_mode split. The
6768
# signal: auth.json marks plan logins as type "oauth"; plus a few provider markers.
6869
self._oauth_providers = self._load_oauth_providers()
69-
self.records_cost = self._probe_records_cost()
70+
self._records_cost: bool | None = None # resolved lazily (records_cost property)
7071

7172
# --- helpers -------------------------------------------------------------
7273
def _git_root(self, cwd: str) -> str:
@@ -186,10 +187,24 @@ def cache_inputs(self) -> list[str]:
186187
def _files(self) -> list[str]:
187188
return glob.glob(os.path.join(self.root_dir, "**", "*.jsonl"), recursive=True)
188189

190+
@property
191+
def records_cost(self) -> bool:
192+
# True iff any *metered* (non-subscription) message records real spend. Lazy so
193+
# construction never reads the corpus (the warm-start cache answers a hit without
194+
# reaching here): after a parse it derives from the accumulated per-model costs;
195+
# the full-file probe runs only when it is read before any parse.
196+
if self._sessions is not None:
197+
return any(
198+
acc["cost"] > 0 for s in self._sessions.values() for acc in s["models"].values()
199+
)
200+
if self._records_cost is None:
201+
self._records_cost = self._probe_records_cost()
202+
return self._records_cost
203+
189204
def _probe_records_cost(self) -> bool:
190205
# True iff any *metered* (non-subscription) assistant message records a positive
191-
# cost. Early-exits so it stays cheap (safe in __init__; CombinedStore reads it
192-
# before workflows()). A subscription-only setup -> False (every cost is estimated).
206+
# cost. Early-exits so it stays cheap. A subscription-only setup -> False (every
207+
# cost is estimated).
193208
for path in self._files():
194209
try:
195210
fh = open(path, encoding="utf-8", errors="replace")

0 commit comments

Comments
 (0)