Skip to content

Commit 4007cfa

Browse files
committed
improve event paging
1 parent 1fd22ed commit 4007cfa

2 files changed

Lines changed: 45 additions & 24 deletions

File tree

‎src/inspect_ai/log/_transcript.py‎

Lines changed: 43 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -177,10 +177,9 @@ def _slice(self, index: slice) -> Sequence[Event]:
177177
if provider is not None:
178178
if start >= 0:
179179
return provider.events_from(start)
180-
if -start <= min(
181-
self._transcript._resident_tail, len(self._transcript._events)
182-
):
183-
return self._transcript._events[start:]
180+
window = self._transcript._trailing_window(-start)
181+
if window is not None:
182+
return window
184183
return provider.recent_events(-start)
185184
return self._materialize()[index]
186185

@@ -287,10 +286,10 @@ def recent_events(self, n: int | None = None) -> Sequence[Event]:
287286
"Full transcript history is not available from this Transcript"
288287
)
289288
return transcript._events if n is None else transcript._events[-n:]
290-
if n is not None and n <= min(
291-
transcript._resident_tail, len(transcript._events)
292-
):
293-
return transcript._events[-n:]
289+
if n is not None:
290+
window = transcript._trailing_window(n)
291+
if window is not None:
292+
return window
294293
return transcript._history_provider.recent_events(n)
295294

296295
def events_from(self, start: int, limit: int | None = None) -> Sequence[Event]:
@@ -324,23 +323,21 @@ def events_from(self, start: int, limit: int | None = None) -> Sequence[Event]:
324323
if start >= count:
325324
return []
326325
if not transcript._events_truncated:
327-
# nothing evicted: resident events ARE the logical history
328-
events: Sequence[Event] = transcript._events[start:]
329-
return events[:limit] if limit is not None else events
326+
# nothing evicted: resident events ARE the logical history. Slice
327+
# page-bounded in one step — copying the whole tail and then
328+
# trimming it would be O(remaining history) per page on large
329+
# transcripts.
330+
end = count if limit is None else start + limit
331+
return transcript._events[start:end]
330332
# Once events have been evicted, resident events are NOT a contiguous
331-
# suffix of the logical history: pinned and pending events survive
332-
# eviction at their insertion positions (eg. a SampleInitEvent ahead
333-
# of the resident tail), so a logical offset can't be mapped into
334-
# ``_events`` by suffix arithmetic. The exception is the trailing
335-
# window ``recent_events`` also relies on: the newest
336-
# ``resident_tail`` logical events are always resident and always the
337-
# last elements of ``_events`` (eviction removes only *older*
338-
# evictable events), so a read confined to that window is a memory
339-
# slice. Anything earlier must come from the provider.
340-
n = count - start
341-
if n <= min(transcript._resident_tail, len(transcript._events)):
342-
events = transcript._events[-n:]
343-
return events[:limit] if limit is not None else events
333+
# suffix of the logical history (pinned/pending events survive at
334+
# their insertion positions), so a logical offset can't be mapped
335+
# into ``_events`` by suffix arithmetic — only the trailing window is
336+
# a memory read (see `Transcript._trailing_window` for the
337+
# invariant). Anything earlier must come from the provider.
338+
window = transcript._trailing_window(count - start, limit)
339+
if window is not None:
340+
return window
344341
if transcript._history_provider is None:
345342
raise TranscriptHistoryUnavailableError(
346343
"Full transcript history is not available from this Transcript"
@@ -698,6 +695,28 @@ def _prune_unreferenced_attachments(self) -> None:
698695
if ref not in self._attachment_refcount:
699696
self._attachments.pop(ref, None)
700697

698+
def _trailing_window(self, n: int, limit: int | None = None) -> list[Event] | None:
699+
"""The newest ``n`` logical events, served from resident memory.
700+
701+
The eviction invariant this relies on — maintained by
702+
:meth:`_evict_events`, and the single place it should be reasoned
703+
about: eviction removes only the *oldest evictable* events, while
704+
pinned and pending events survive at their insertion positions. Older
705+
positions in ``_events`` may therefore be gapped relative to the
706+
logical history, but the trailing ``min(_resident_tail,
707+
len(_events))`` elements are always exactly the newest logical
708+
events, in order.
709+
710+
Returns the window (optionally capped at ``limit`` events from its
711+
start) when ``n`` lies within that guarantee, else ``None`` — the
712+
caller must then materialize from the history provider (or fail).
713+
"""
714+
if n > min(self._resident_tail, len(self._events)):
715+
return None
716+
first = len(self._events) - n
717+
end = len(self._events) if limit is None else first + limit
718+
return self._events[first:end]
719+
701720
def _evict_events(self) -> None:
702721
if not self._bounded:
703722
return

‎tests/log/test_transcript_bounded.py‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -816,6 +816,7 @@ def test_history_events_from_serves_resident_window_without_provider() -> None:
816816
# resident window is [2, 3, 4]; start inside it slices memory
817817
assert _data(transcript.history.events_from(3)) == [3, 4]
818818
assert _data(transcript.history.events_from(3, limit=1)) == [3]
819+
assert _data(transcript.history.events_from(3, limit=100)) == [3, 4] # clamps
819820
assert transcript.history.events_from(5) == [] # at/past the end
820821

821822

@@ -850,6 +851,7 @@ def test_history_events_from_unbounded_transcript() -> None:
850851
transcript._event(InfoEvent(data=data))
851852

852853
assert _data(transcript.history.events_from(0)) == [0, 1, 2]
854+
assert _data(transcript.history.events_from(1, limit=100)) == [1, 2] # clamps
853855
assert _data(transcript.history.events_from(-5, limit=2)) == [0, 1] # clamps
854856

855857

0 commit comments

Comments
 (0)