mirror of
https://github.com/openai/codex.git
synced 2026-11-13 14:46:14 +00:00
Track compaction time in turn profiles (#34835)
## What changed - Add `compaction_ms` to turn profile facts and turn analytics events. - Measure manual and automatic compaction as an exclusive profile phase instead of including it in surrounding idle time. - Complete the profile and total turn duration from the same instant so their elapsed times remain consistent. ## Testing - Cover compaction phase accounting and profile-duration consistency. - Update turn event serialization coverage for `compaction_ms`. GitOrigin-RevId: 50f0e1cf19789a807f9f56c2ef763c620a5d641c
This commit is contained in:
1 parent
88f1cd9664
commit
bd5b55e403
9 files changed
+114
-18
No files matched your search
@@ -441,9 +441,10 @@ fn sample_turn_profile() -> TurnProfile {
|
|||||||
TurnProfile {
|
TurnProfile {
|
||||||
before_first_sampling_ms: 100,
|
before_first_sampling_ms: 100,
|
||||||
sampling_ms: 700,
|
sampling_ms: 700,
|
||||||
|
compaction_ms: 40,
|
||||||
between_sampling_overhead_ms: 50,
|
between_sampling_overhead_ms: 50,
|
||||||
tool_blocking_ms: 250,
|
tool_blocking_ms: 250,
|
||||||
after_last_sampling_ms: 134,
|
after_last_sampling_ms: 94,
|
||||||
sampling_request_count: 2,
|
sampling_request_count: 2,
|
||||||
sampling_retry_count: 1,
|
sampling_retry_count: 1,
|
||||||
}
|
}
|
||||||
@@ -4065,9 +4066,10 @@ fn turn_event_serializes_expected_shape() {
|
|||||||
total_tokens: None,
|
total_tokens: None,
|
||||||
before_first_sampling_ms: 100,
|
before_first_sampling_ms: 100,
|
||||||
sampling_ms: 700,
|
sampling_ms: 700,
|
||||||
|
compaction_ms: 40,
|
||||||
between_sampling_overhead_ms: 50,
|
between_sampling_overhead_ms: 50,
|
||||||
tool_blocking_ms: 250,
|
tool_blocking_ms: 250,
|
||||||
after_last_sampling_ms: 134,
|
after_last_sampling_ms: 94,
|
||||||
sampling_request_count: 2,
|
sampling_request_count: 2,
|
||||||
sampling_retry_count: 1,
|
sampling_retry_count: 1,
|
||||||
duration_ms: Some(1234),
|
duration_ms: Some(1234),
|
||||||
@@ -4138,9 +4140,10 @@ fn turn_event_serializes_expected_shape() {
|
|||||||
"total_tokens": null,
|
"total_tokens": null,
|
||||||
"before_first_sampling_ms": 100,
|
"before_first_sampling_ms": 100,
|
||||||
"sampling_ms": 700,
|
"sampling_ms": 700,
|
||||||
|
"compaction_ms": 40,
|
||||||
"between_sampling_overhead_ms": 50,
|
"between_sampling_overhead_ms": 50,
|
||||||
"tool_blocking_ms": 250,
|
"tool_blocking_ms": 250,
|
||||||
"after_last_sampling_ms": 134,
|
"after_last_sampling_ms": 94,
|
||||||
"sampling_request_count": 2,
|
"sampling_request_count": 2,
|
||||||
"sampling_retry_count": 1,
|
"sampling_retry_count": 1,
|
||||||
"duration_ms": 1234,
|
"duration_ms": 1234,
|
||||||
|
|||||||
@@ -914,6 +914,7 @@ pub(crate) struct CodexTurnEventParams {
|
|||||||
pub(crate) total_tokens: Option<i64>,
|
pub(crate) total_tokens: Option<i64>,
|
||||||
pub(crate) before_first_sampling_ms: u64,
|
pub(crate) before_first_sampling_ms: u64,
|
||||||
pub(crate) sampling_ms: u64,
|
pub(crate) sampling_ms: u64,
|
||||||
|
pub(crate) compaction_ms: u64,
|
||||||
pub(crate) between_sampling_overhead_ms: u64,
|
pub(crate) between_sampling_overhead_ms: u64,
|
||||||
pub(crate) tool_blocking_ms: u64,
|
pub(crate) tool_blocking_ms: u64,
|
||||||
pub(crate) after_last_sampling_ms: u64,
|
pub(crate) after_last_sampling_ms: u64,
|
||||||
|
|||||||
@@ -108,6 +108,7 @@ pub struct TurnTokenUsageFact {
|
|||||||
pub struct TurnProfile {
|
pub struct TurnProfile {
|
||||||
pub before_first_sampling_ms: u64,
|
pub before_first_sampling_ms: u64,
|
||||||
pub sampling_ms: u64,
|
pub sampling_ms: u64,
|
||||||
|
pub compaction_ms: u64,
|
||||||
pub between_sampling_overhead_ms: u64,
|
pub between_sampling_overhead_ms: u64,
|
||||||
pub tool_blocking_ms: u64,
|
pub tool_blocking_ms: u64,
|
||||||
pub after_last_sampling_ms: u64,
|
pub after_last_sampling_ms: u64,
|
||||||
|
|||||||
@@ -2636,6 +2636,7 @@ fn codex_turn_event_params(
|
|||||||
let TurnProfile {
|
let TurnProfile {
|
||||||
before_first_sampling_ms,
|
before_first_sampling_ms,
|
||||||
sampling_ms,
|
sampling_ms,
|
||||||
|
compaction_ms,
|
||||||
between_sampling_overhead_ms,
|
between_sampling_overhead_ms,
|
||||||
tool_blocking_ms,
|
tool_blocking_ms,
|
||||||
after_last_sampling_ms,
|
after_last_sampling_ms,
|
||||||
@@ -2708,6 +2709,7 @@ fn codex_turn_event_params(
|
|||||||
.map(|token_usage| token_usage.total_tokens),
|
.map(|token_usage| token_usage.total_tokens),
|
||||||
before_first_sampling_ms,
|
before_first_sampling_ms,
|
||||||
sampling_ms,
|
sampling_ms,
|
||||||
|
compaction_ms,
|
||||||
between_sampling_overhead_ms,
|
between_sampling_overhead_ms,
|
||||||
tool_blocking_ms,
|
tool_blocking_ms,
|
||||||
after_last_sampling_ms,
|
after_last_sampling_ms,
|
||||||
|
|||||||
@@ -989,6 +989,7 @@ async fn run_auto_compact(
|
|||||||
phase: CompactionPhase,
|
phase: CompactionPhase,
|
||||||
) -> CodexResult<()> {
|
) -> CodexResult<()> {
|
||||||
let turn_context = &step_context.turn;
|
let turn_context = &step_context.turn;
|
||||||
|
let _profile_guard = turn_context.turn_timing_state.begin_compaction();
|
||||||
if turn_context.config.features.enabled(Feature::TokenBudget) {
|
if turn_context.config.features.enabled(Feature::TokenBudget) {
|
||||||
// Compaction is the reset request, so force a new context window
|
// Compaction is the reset request, so force a new context window
|
||||||
// instead of consuming a pending `new_context` tool request.
|
// instead of consuming a pending `new_context` tool request.
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ impl SessionTask for CompactTask {
|
|||||||
_cancellation_token: CancellationToken,
|
_cancellation_token: CancellationToken,
|
||||||
) -> SessionTaskResult {
|
) -> SessionTaskResult {
|
||||||
let session = session.clone_session();
|
let session = session.clone_session();
|
||||||
|
let _profile_guard = ctx.turn_timing_state.begin_compaction();
|
||||||
if ctx.config.features.enabled(Feature::TokenBudget) {
|
if ctx.config.features.enabled(Feature::TokenBudget) {
|
||||||
crate::compact_token_budget::run_manual_compact_task(session, ctx).await?;
|
crate::compact_token_budget::run_manual_compact_task(session, ctx).await?;
|
||||||
return Ok(None);
|
return Ok(None);
|
||||||
|
|||||||
@@ -758,15 +758,15 @@ impl Session {
|
|||||||
turn_had_memory_citation,
|
turn_had_memory_citation,
|
||||||
);
|
);
|
||||||
let started_at = turn_context.turn_timing_state.started_at_unix_secs().await;
|
let started_at = turn_context.turn_timing_state.started_at_unix_secs().await;
|
||||||
let (completed_at, duration_ms) = turn_context
|
let (completed_at, duration_ms, profile) = turn_context
|
||||||
.turn_timing_state
|
.turn_timing_state
|
||||||
.completed_at_and_duration_ms()
|
.complete_profile_and_duration_ms()
|
||||||
.await;
|
.await;
|
||||||
self.services
|
self.services
|
||||||
.analytics_events_client
|
.analytics_events_client
|
||||||
.track_turn_profile(TurnProfileFact {
|
.track_turn_profile(TurnProfileFact {
|
||||||
turn_id: turn_context.sub_id.clone(),
|
turn_id: turn_context.sub_id.clone(),
|
||||||
profile: turn_context.turn_timing_state.complete_profile(),
|
profile,
|
||||||
});
|
});
|
||||||
let event = if let Some(reason) = abort_reason {
|
let event = if let Some(reason) = abort_reason {
|
||||||
self.emit_turn_abort_lifecycle(reason.clone(), turn_context.extension_data.as_ref())
|
self.emit_turn_abort_lifecycle(reason.clone(), turn_context.extension_data.as_ref())
|
||||||
@@ -907,16 +907,16 @@ impl Session {
|
|||||||
.turn_timing_state
|
.turn_timing_state
|
||||||
.started_at_unix_secs()
|
.started_at_unix_secs()
|
||||||
.await;
|
.await;
|
||||||
let (completed_at, duration_ms) = task
|
let (completed_at, duration_ms, profile) = task
|
||||||
.turn_context
|
.turn_context
|
||||||
.turn_timing_state
|
.turn_timing_state
|
||||||
.completed_at_and_duration_ms()
|
.complete_profile_and_duration_ms()
|
||||||
.await;
|
.await;
|
||||||
self.services
|
self.services
|
||||||
.analytics_events_client
|
.analytics_events_client
|
||||||
.track_turn_profile(TurnProfileFact {
|
.track_turn_profile(TurnProfileFact {
|
||||||
turn_id: task.turn_context.sub_id.clone(),
|
turn_id: task.turn_context.sub_id.clone(),
|
||||||
profile: task.turn_context.turn_timing_state.complete_profile(),
|
profile,
|
||||||
});
|
});
|
||||||
let event = EventMsg::TurnAborted(TurnAbortedEvent {
|
let event = EventMsg::TurnAborted(TurnAbortedEvent {
|
||||||
turn_id: Some(task.turn_context.sub_id.clone()),
|
turn_id: Some(task.turn_context.sub_id.clone()),
|
||||||
|
|||||||
@@ -61,6 +61,7 @@ struct TurnProfileState {
|
|||||||
seen_sampling: bool,
|
seen_sampling: bool,
|
||||||
before_first_sampling: Duration,
|
before_first_sampling: Duration,
|
||||||
sampling: Duration,
|
sampling: Duration,
|
||||||
|
compaction: Duration,
|
||||||
between_sampling_overhead: Duration,
|
between_sampling_overhead: Duration,
|
||||||
tool_blocking: Duration,
|
tool_blocking: Duration,
|
||||||
pending_idle_after_sampling: Duration,
|
pending_idle_after_sampling: Duration,
|
||||||
@@ -72,6 +73,7 @@ struct TurnProfileState {
|
|||||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||||
enum TurnProfilePhase {
|
enum TurnProfilePhase {
|
||||||
Sampling,
|
Sampling,
|
||||||
|
Compaction,
|
||||||
ToolBlocking,
|
ToolBlocking,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -98,13 +100,22 @@ impl TurnTimingState {
|
|||||||
self.state.lock().await.started_at_unix_secs
|
self.state.lock().await.started_at_unix_secs
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) async fn completed_at_and_duration_ms(&self) -> (Option<i64>, Option<i64>) {
|
pub(crate) async fn complete_profile_and_duration_ms(
|
||||||
|
&self,
|
||||||
|
) -> (Option<i64>, Option<i64>, TurnProfile) {
|
||||||
|
let completed_at_instant = Instant::now();
|
||||||
let state = self.state.lock().await;
|
let state = self.state.lock().await;
|
||||||
let completed_at = Some(now_unix_timestamp_secs());
|
let completed_at = Some(now_unix_timestamp_secs());
|
||||||
let duration_ms = state
|
let duration_ms = state.started_at.map(|started_at| {
|
||||||
.started_at
|
i64::try_from(
|
||||||
.map(|started_at| i64::try_from(started_at.elapsed().as_millis()).unwrap_or(i64::MAX));
|
completed_at_instant
|
||||||
(completed_at, duration_ms)
|
.saturating_duration_since(started_at)
|
||||||
|
.as_millis(),
|
||||||
|
)
|
||||||
|
.unwrap_or(i64::MAX)
|
||||||
|
});
|
||||||
|
let profile = self.profile_state().complete(completed_at_instant);
|
||||||
|
(completed_at, duration_ms, profile)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) async fn time_to_first_token_ms(&self) -> Option<i64> {
|
pub(crate) async fn time_to_first_token_ms(&self) -> Option<i64> {
|
||||||
@@ -114,10 +125,6 @@ impl TurnTimingState {
|
|||||||
.map(|duration| i64::try_from(duration.as_millis()).unwrap_or(i64::MAX))
|
.map(|duration| i64::try_from(duration.as_millis()).unwrap_or(i64::MAX))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn complete_profile(&self) -> TurnProfile {
|
|
||||||
self.profile_state().complete(Instant::now())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn begin_sampling(self: &Arc<Self>) -> TurnProfileTimingGuard {
|
pub(crate) fn begin_sampling(self: &Arc<Self>) -> TurnProfileTimingGuard {
|
||||||
let active = self.profile_state().begin_sampling(Instant::now());
|
let active = self.profile_state().begin_sampling(Instant::now());
|
||||||
TurnProfileTimingGuard {
|
TurnProfileTimingGuard {
|
||||||
@@ -131,6 +138,15 @@ impl TurnTimingState {
|
|||||||
self.profile_state().record_sampling_retry();
|
self.profile_state().record_sampling_retry();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn begin_compaction(self: &Arc<Self>) -> TurnProfileTimingGuard {
|
||||||
|
let active = self.profile_state().begin_compaction(Instant::now());
|
||||||
|
TurnProfileTimingGuard {
|
||||||
|
timing: Arc::clone(self),
|
||||||
|
phase: TurnProfilePhase::Compaction,
|
||||||
|
active,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) fn begin_tool_blocking(self: &Arc<Self>) -> TurnProfileTimingGuard {
|
pub(crate) fn begin_tool_blocking(self: &Arc<Self>) -> TurnProfileTimingGuard {
|
||||||
let active = self.profile_state().begin_tool_blocking(Instant::now());
|
let active = self.profile_state().begin_tool_blocking(Instant::now());
|
||||||
TurnProfileTimingGuard {
|
TurnProfileTimingGuard {
|
||||||
@@ -235,6 +251,18 @@ impl TurnProfileState {
|
|||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn begin_compaction(&mut self, now: Instant) -> bool {
|
||||||
|
if self.completed_profile.is_some()
|
||||||
|
|| self.started_at.is_none()
|
||||||
|
|| self.active_phase.is_some()
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
self.advance(now);
|
||||||
|
self.active_phase = Some(TurnProfilePhase::Compaction);
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
fn end_phase(&mut self, now: Instant, phase: TurnProfilePhase) {
|
fn end_phase(&mut self, now: Instant, phase: TurnProfilePhase) {
|
||||||
if self.completed_profile.is_some() || self.active_phase != Some(phase) {
|
if self.completed_profile.is_some() || self.active_phase != Some(phase) {
|
||||||
return;
|
return;
|
||||||
@@ -250,6 +278,7 @@ impl TurnProfileState {
|
|||||||
let elapsed = now.saturating_duration_since(previous);
|
let elapsed = now.saturating_duration_since(previous);
|
||||||
match self.active_phase {
|
match self.active_phase {
|
||||||
Some(TurnProfilePhase::Sampling) => self.sampling += elapsed,
|
Some(TurnProfilePhase::Sampling) => self.sampling += elapsed,
|
||||||
|
Some(TurnProfilePhase::Compaction) => self.compaction += elapsed,
|
||||||
Some(TurnProfilePhase::ToolBlocking) => self.tool_blocking += elapsed,
|
Some(TurnProfilePhase::ToolBlocking) => self.tool_blocking += elapsed,
|
||||||
None if self.seen_sampling => self.pending_idle_after_sampling += elapsed,
|
None if self.seen_sampling => self.pending_idle_after_sampling += elapsed,
|
||||||
None => self.before_first_sampling += elapsed,
|
None => self.before_first_sampling += elapsed,
|
||||||
@@ -272,6 +301,7 @@ impl TurnProfileState {
|
|||||||
let mut profile = TurnProfile {
|
let mut profile = TurnProfile {
|
||||||
before_first_sampling_ms: duration_to_u64_ms(self.before_first_sampling),
|
before_first_sampling_ms: duration_to_u64_ms(self.before_first_sampling),
|
||||||
sampling_ms: duration_to_u64_ms(self.sampling),
|
sampling_ms: duration_to_u64_ms(self.sampling),
|
||||||
|
compaction_ms: duration_to_u64_ms(self.compaction),
|
||||||
between_sampling_overhead_ms: duration_to_u64_ms(self.between_sampling_overhead),
|
between_sampling_overhead_ms: duration_to_u64_ms(self.between_sampling_overhead),
|
||||||
tool_blocking_ms: duration_to_u64_ms(self.tool_blocking),
|
tool_blocking_ms: duration_to_u64_ms(self.tool_blocking),
|
||||||
after_last_sampling_ms: duration_to_u64_ms(after_last_sampling),
|
after_last_sampling_ms: duration_to_u64_ms(after_last_sampling),
|
||||||
@@ -285,12 +315,14 @@ impl TurnProfileState {
|
|||||||
let classified_ms = profile
|
let classified_ms = profile
|
||||||
.before_first_sampling_ms
|
.before_first_sampling_ms
|
||||||
.saturating_add(profile.sampling_ms)
|
.saturating_add(profile.sampling_ms)
|
||||||
|
.saturating_add(profile.compaction_ms)
|
||||||
.saturating_add(profile.between_sampling_overhead_ms)
|
.saturating_add(profile.between_sampling_overhead_ms)
|
||||||
.saturating_add(profile.tool_blocking_ms)
|
.saturating_add(profile.tool_blocking_ms)
|
||||||
.saturating_add(profile.after_last_sampling_ms);
|
.saturating_add(profile.after_last_sampling_ms);
|
||||||
let rounding_ms = total_ms.saturating_sub(classified_ms);
|
let rounding_ms = total_ms.saturating_sub(classified_ms);
|
||||||
match final_phase {
|
match final_phase {
|
||||||
Some(TurnProfilePhase::Sampling) => profile.sampling_ms += rounding_ms,
|
Some(TurnProfilePhase::Sampling) => profile.sampling_ms += rounding_ms,
|
||||||
|
Some(TurnProfilePhase::Compaction) => profile.compaction_ms += rounding_ms,
|
||||||
Some(TurnProfilePhase::ToolBlocking) => profile.tool_blocking_ms += rounding_ms,
|
Some(TurnProfilePhase::ToolBlocking) => profile.tool_blocking_ms += rounding_ms,
|
||||||
None if self.seen_sampling => profile.after_last_sampling_ms += rounding_ms,
|
None if self.seen_sampling => profile.after_last_sampling_ms += rounding_ms,
|
||||||
None => profile.before_first_sampling_ms += rounding_ms,
|
None => profile.before_first_sampling_ms += rounding_ms,
|
||||||
|
|||||||
@@ -186,6 +186,7 @@ fn turn_profile_breaks_down_sampling_blocking_and_retry_overhead() {
|
|||||||
TurnProfile {
|
TurnProfile {
|
||||||
before_first_sampling_ms: 100,
|
before_first_sampling_ms: 100,
|
||||||
sampling_ms: 700,
|
sampling_ms: 700,
|
||||||
|
compaction_ms: 0,
|
||||||
between_sampling_overhead_ms: 100,
|
between_sampling_overhead_ms: 100,
|
||||||
tool_blocking_ms: 300,
|
tool_blocking_ms: 300,
|
||||||
after_last_sampling_ms: 100,
|
after_last_sampling_ms: 100,
|
||||||
@@ -194,3 +195,57 @@ fn turn_profile_breaks_down_sampling_blocking_and_retry_overhead() {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn turn_profile_counts_compaction_as_an_exclusive_phase() {
|
||||||
|
let started_at = Instant::now();
|
||||||
|
let mut state = TurnProfileState::default();
|
||||||
|
state.start(started_at);
|
||||||
|
|
||||||
|
let _ = state.begin_compaction(started_at + Duration::from_millis(100));
|
||||||
|
state.end_phase(
|
||||||
|
started_at + Duration::from_millis(300),
|
||||||
|
TurnProfilePhase::Compaction,
|
||||||
|
);
|
||||||
|
let _ = state.begin_sampling(started_at + Duration::from_millis(400));
|
||||||
|
state.end_phase(
|
||||||
|
started_at + Duration::from_millis(700),
|
||||||
|
TurnProfilePhase::Sampling,
|
||||||
|
);
|
||||||
|
let _ = state.begin_compaction(started_at + Duration::from_millis(800));
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
state.complete(started_at + Duration::from_millis(1_100)),
|
||||||
|
TurnProfile {
|
||||||
|
before_first_sampling_ms: 200,
|
||||||
|
sampling_ms: 300,
|
||||||
|
compaction_ms: 500,
|
||||||
|
between_sampling_overhead_ms: 0,
|
||||||
|
tool_blocking_ms: 0,
|
||||||
|
after_last_sampling_ms: 100,
|
||||||
|
sampling_request_count: 1,
|
||||||
|
sampling_retry_count: 0,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn turn_profile_and_duration_share_a_completion_instant() {
|
||||||
|
let state = TurnTimingState::default();
|
||||||
|
state
|
||||||
|
.mark_turn_started(Instant::now() - Duration::from_millis(100))
|
||||||
|
.await;
|
||||||
|
|
||||||
|
let (_, duration_ms, profile) = state.complete_profile_and_duration_ms().await;
|
||||||
|
let classified_ms = profile.before_first_sampling_ms
|
||||||
|
+ profile.sampling_ms
|
||||||
|
+ profile.compaction_ms
|
||||||
|
+ profile.between_sampling_overhead_ms
|
||||||
|
+ profile.tool_blocking_ms
|
||||||
|
+ profile.after_last_sampling_ms;
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
duration_ms,
|
||||||
|
Some(i64::try_from(classified_ms).expect("profile duration should fit i64"))
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in new issue
Block a user