From f1eaca7d16d0e1a26d473a961295745757a1a4a4 Mon Sep 17 00:00:00 2001 From: Chris Huber Date: Sun, 7 Jun 2026 09:11:47 -0400 Subject: [PATCH 1/2] fix: cache active worktree classifier probes --- inc/Workspace/Workspace.php | 153 +++++++++++++++++--- tests/smoke-worktree-metadata-reconcile.php | 13 ++ 2 files changed, 148 insertions(+), 18 deletions(-) diff --git a/inc/Workspace/Workspace.php b/inc/Workspace/Workspace.php index d6673501..7e7a6b88 100644 --- a/inc/Workspace/Workspace.php +++ b/inc/Workspace/Workspace.php @@ -1655,6 +1655,18 @@ public function worktree_active_no_signal_report( array $opts = array() ): array $page = array_slice($active, $offset, $limit); $github_cache = array(); + $probe_cache = array( + 'default_ref' => array(), + 'github_slug' => array(), + 'remote_tracking' => array(), + 'commits_outside_default' => array(), + 'stats' => array( + 'default_ref' => array( 'hits' => 0, 'misses' => 0 ), + 'github_slug' => array( 'hits' => 0, 'misses' => 0 ), + 'remote_tracking' => array( 'hits' => 0, 'misses' => 0 ), + 'commits_outside_default' => array( 'hits' => 0, 'misses' => 0 ), + ), + ); $rows = array(); $summary = array( 'total_active_no_signal' => $total, @@ -1674,7 +1686,7 @@ public function worktree_active_no_signal_report( array $opts = array() ): array break; } $row_started = microtime(true); - $evidence = $this->build_active_no_signal_evidence_row($row, $github_cache); + $evidence = $this->build_active_no_signal_evidence_row($row, $github_cache, $probe_cache); $evidence['elapsed_ms'] = (int) round(( microtime(true) - $row_started ) * 1000); $rows[] = $evidence; ++$summary['inspected']; @@ -1716,9 +1728,10 @@ public function worktree_active_no_signal_report( array $opts = array() ): array 'summary' => array_merge($summary, array( 'slow_rows' => $this->summarize_slow_worktree_rows($rows) )), 'pagination' => $pagination, 'evidence' => array( - 'scope' => 'review-only active_no_signal worktree lifecycle evidence', - 'safety' => 'No worktrees or remote branches are deleted. Dirty and unpushed probes are evidence only.', - 'budget' => null === $budget_context ? null : $this->summarize_worktree_loop_budget_context($budget_context, $budget_stopped), + 'scope' => 'review-only active_no_signal worktree lifecycle evidence', + 'safety' => 'No worktrees or remote branches are deleted. Dirty and unpushed probes are evidence only.', + 'budget' => null === $budget_context ? null : $this->summarize_worktree_loop_budget_context($budget_context, $budget_stopped), + 'probe_cache' => $probe_cache['stats'], ), ); } @@ -2672,9 +2685,10 @@ private function build_active_no_signal_finalized_apply_skip( array $row, string * * @param array $row Inventory skip row. * @param array $github_cache Run-local GitHub cache. + * @param array $probe_cache Run-local git probe cache. * @return array */ - private function build_active_no_signal_evidence_row( array $row, array &$github_cache ): array { + private function build_active_no_signal_evidence_row( array $row, array &$github_cache, array &$probe_cache ): array { $handle = (string) ( $row['handle'] ?? '' ); $repo = (string) ( $row['repo'] ?? '' ); $branch = (string) ( $row['branch'] ?? '' ); @@ -2684,11 +2698,16 @@ private function build_active_no_signal_evidence_row( array $row, array &$github $metadata = is_array($row['metadata'] ?? null) ? $row['metadata'] : array(); $branch_probe = null; if ( '' !== $path && is_dir($path) ) { - $branch_probe = $this->run_git($path, 'branch --show-current', self::CLEANUP_GIT_PROBE_TIMEOUT); - if ( ! is_wp_error($branch_probe) && ! $this->is_git_timeout_error($branch_probe) ) { - $actual_branch = trim( (string) ( $branch_probe['output'] ?? '' ) ); - if ( '' !== $actual_branch ) { - $branch = $actual_branch; + $head_branch = $this->resolve_worktree_branch_from_head_file($path); + if ( null !== $head_branch && '' !== $head_branch ) { + $branch = $head_branch; + } else { + $branch_probe = $this->run_git($path, 'branch --show-current', self::CLEANUP_GIT_PROBE_TIMEOUT); + if ( ! is_wp_error($branch_probe) && ! $this->is_git_timeout_error($branch_probe) ) { + $actual_branch = trim( (string) ( $branch_probe['output'] ?? '' ) ); + if ( '' !== $actual_branch ) { + $branch = $actual_branch; + } } } } @@ -2749,18 +2768,16 @@ private function build_active_no_signal_evidence_row( array $row, array &$github } $remote_ref = 'refs/remotes/origin/' . $branch; - $remote = $this->time_worktree_probe($out['probe_timings_ms'], 'remote_tracking', fn() => $this->run_git($primary_path, sprintf('rev-parse --verify --quiet %s', escapeshellarg($remote_ref)), self::CLEANUP_GIT_PROBE_TIMEOUT)); + $remote = $this->time_worktree_probe($out['probe_timings_ms'], 'remote_tracking', fn() => $this->cached_active_no_signal_remote_tracking_probe($primary_path, $remote_ref, $probe_cache)); $out['remote_tracking'] = ! is_wp_error($remote) && ! $this->is_git_timeout_error($remote); - $default_ref = $this->time_worktree_probe($out['probe_timings_ms'], 'default_ref', fn() => $this->resolve_remote_default_ref($primary_path, self::CLEANUP_GIT_PROBE_TIMEOUT)); + $default_ref = $this->time_worktree_probe($out['probe_timings_ms'], 'default_ref', fn() => $this->cached_active_no_signal_default_ref_probe($primary_path, $probe_cache)); if ( is_string($default_ref) && '' !== $default_ref ) { $out['default_ref'] = $default_ref; $outside = $this->time_worktree_probe( - $out['probe_timings_ms'], 'commits_outside_default', fn() => $this->run_git( - $primary_path, - sprintf('rev-list --count %s..%s', escapeshellarg($default_ref), escapeshellarg('refs/heads/' . $branch)), - self::CLEANUP_GIT_PROBE_TIMEOUT - ) + $out['probe_timings_ms'], + 'commits_outside_default', + fn() => $this->cached_active_no_signal_commits_outside_default_probe($primary_path, $default_ref, $branch, $probe_cache) ); if ( ! is_wp_error($outside) && ! $this->is_git_timeout_error($outside) ) { $out['commits_outside_default'] = (int) trim( (string) ( $outside['output'] ?? '' )); @@ -2776,7 +2793,7 @@ private function build_active_no_signal_evidence_row( array $row, array &$github if ( (int) ( $out['dirty'] ?? 0 ) > 0 || (int) ( $out['unpushed'] ?? 0 ) > 0 ) { $out['pr_lookup_skipped'] = 'dirty_or_unpushed_rows_are_always_manual_review'; } else { - $slug = $this->time_worktree_probe($out['probe_timings_ms'], 'github_slug', fn() => $this->resolve_github_slug($primary_path)); + $slug = $this->time_worktree_probe($out['probe_timings_ms'], 'github_slug', fn() => $this->cached_active_no_signal_github_slug_probe($primary_path, $probe_cache)); if ( null !== $slug ) { $pr = $this->time_worktree_probe($out['probe_timings_ms'], 'github_pr_lookup', fn() => $this->find_pr_for_branch_direct($slug, $branch, $github_cache, false)); if ( is_wp_error($pr) ) { @@ -2793,6 +2810,106 @@ private function build_active_no_signal_evidence_row( array $row, array &$github return $out; } + /** + * Cache a remote-tracking ref existence probe for an active/no-signal report run. + * + * @param string $primary_path Primary checkout path. + * @param string $remote_ref Fully-qualified remote-tracking ref. + * @param array $probe_cache Run-local git probe cache. + * @return array|\WP_Error Git result or timeout/error. + */ + private function cached_active_no_signal_remote_tracking_probe( string $primary_path, string $remote_ref, array &$probe_cache ): array|\WP_Error { + $key = $primary_path . '#' . $remote_ref; + if ( array_key_exists($key, $probe_cache['remote_tracking'] ?? array()) ) { + $this->record_active_no_signal_probe_cache_stat($probe_cache, 'remote_tracking', true); + return $probe_cache['remote_tracking'][ $key ]; + } + + $this->record_active_no_signal_probe_cache_stat($probe_cache, 'remote_tracking', false); + $result = $this->run_git($primary_path, sprintf('rev-parse --verify --quiet %s', escapeshellarg($remote_ref)), self::CLEANUP_GIT_PROBE_TIMEOUT); + $probe_cache['remote_tracking'][ $key ] = $result; + return $result; + } + + /** + * Cache the remote default ref for an active/no-signal report run. + * + * @param string $primary_path Primary checkout path. + * @param array $probe_cache Run-local git probe cache. + * @return string|\WP_Error|null Fully-qualified remote default ref, timeout/error, or null. + */ + private function cached_active_no_signal_default_ref_probe( string $primary_path, array &$probe_cache ): string|\WP_Error|null { + if ( array_key_exists($primary_path, $probe_cache['default_ref'] ?? array()) ) { + $this->record_active_no_signal_probe_cache_stat($probe_cache, 'default_ref', true); + return $probe_cache['default_ref'][ $primary_path ]; + } + + $this->record_active_no_signal_probe_cache_stat($probe_cache, 'default_ref', false); + $result = $this->resolve_remote_default_ref($primary_path, self::CLEANUP_GIT_PROBE_TIMEOUT); + $probe_cache['default_ref'][ $primary_path ] = $result; + return $result; + } + + /** + * Cache commits-outside-default probes for an active/no-signal report run. + * + * @param string $primary_path Primary checkout path. + * @param string $default_ref Fully-qualified remote default ref. + * @param string $branch Local branch name. + * @param array $probe_cache Run-local git probe cache. + * @return array|\WP_Error Git result or timeout/error. + */ + private function cached_active_no_signal_commits_outside_default_probe( string $primary_path, string $default_ref, string $branch, array &$probe_cache ): array|\WP_Error { + $key = $primary_path . '#' . $default_ref . '#' . $branch; + if ( array_key_exists($key, $probe_cache['commits_outside_default'] ?? array()) ) { + $this->record_active_no_signal_probe_cache_stat($probe_cache, 'commits_outside_default', true); + return $probe_cache['commits_outside_default'][ $key ]; + } + + $this->record_active_no_signal_probe_cache_stat($probe_cache, 'commits_outside_default', false); + $result = $this->run_git( + $primary_path, + sprintf('rev-list --count %s..%s', escapeshellarg($default_ref), escapeshellarg('refs/heads/' . $branch)), + self::CLEANUP_GIT_PROBE_TIMEOUT + ); + $probe_cache['commits_outside_default'][ $key ] = $result; + return $result; + } + + /** + * Cache the GitHub slug for an active/no-signal report run. + * + * @param string $primary_path Primary checkout path. + * @param array $probe_cache Run-local git probe cache. + * @return string|null owner/repo or null when origin is not GitHub. + */ + private function cached_active_no_signal_github_slug_probe( string $primary_path, array &$probe_cache ): ?string { + if ( array_key_exists($primary_path, $probe_cache['github_slug'] ?? array()) ) { + $this->record_active_no_signal_probe_cache_stat($probe_cache, 'github_slug', true); + return $probe_cache['github_slug'][ $primary_path ]; + } + + $this->record_active_no_signal_probe_cache_stat($probe_cache, 'github_slug', false); + $result = $this->resolve_github_slug($primary_path); + $probe_cache['github_slug'][ $primary_path ] = $result; + return $result; + } + + /** + * Record active/no-signal probe cache hit/miss counts. + * + * @param array $probe_cache Run-local git probe cache. + * @param string $bucket Probe cache bucket. + * @param bool $hit Whether the lookup was a cache hit. + */ + private function record_active_no_signal_probe_cache_stat( array &$probe_cache, string $bucket, bool $hit ): void { + $field = $hit ? 'hits' : 'misses'; + if ( ! isset($probe_cache['stats'][ $bucket ][ $field ]) ) { + $probe_cache['stats'][ $bucket ][ $field ] = 0; + } + ++$probe_cache['stats'][ $bucket ][ $field ]; + } + /** * Build patch-equivalence evidence for clean active/no-signal worktrees. * diff --git a/tests/smoke-worktree-metadata-reconcile.php b/tests/smoke-worktree-metadata-reconcile.php index c7c7d684..2de843bc 100644 --- a/tests/smoke-worktree-metadata-reconcile.php +++ b/tests/smoke-worktree-metadata-reconcile.php @@ -524,6 +524,18 @@ function () use ( $tmp ) { ) ); $ws = new \DataMachineCode\Workspace\Workspace(); + $default_ref_cache_reflection = new \ReflectionMethod($ws, 'cached_active_no_signal_default_ref_probe'); + $default_ref_probe_cache = array( + 'default_ref' => array(), + 'stats' => array( + 'default_ref' => array( 'hits' => 0, 'misses' => 0 ), + ), + ); + $first_default_ref = $default_ref_cache_reflection->invokeArgs($ws, array( $primary, &$default_ref_probe_cache )); + $second_default_ref = $default_ref_cache_reflection->invokeArgs($ws, array( $primary, &$default_ref_probe_cache )); + $assert($first_default_ref, $second_default_ref, 'active/no-signal default ref cache returns stable cached values'); + $assert(1, (int) ( $default_ref_probe_cache['stats']['default_ref']['hits'] ?? 0 ), 'active/no-signal default ref cache records one hit after reuse'); + $assert(1, (int) ( $default_ref_probe_cache['stats']['default_ref']['misses'] ?? 0 ), 'active/no-signal default ref cache records one miss before reuse'); $lookup_reflection = new \ReflectionMethod($ws, 'find_closed_pr_for_branch'); $lookup_cache = array( 'acme/demo' => array() ); $old_pr = $lookup_reflection->invokeArgs($ws, array( 'acme/demo', 'old-merged-branch', &$lookup_cache )); @@ -537,6 +549,7 @@ function () use ( $tmp ) { $assert(true, ! is_wp_error($active_report) && ( $active_report['success'] ?? false ), 'active/no-signal report succeeds'); $assert(true, (bool) ( $active_report['review_only'] ?? false ), 'active/no-signal report is review-only'); $assert(true, (int) ( $active_report['summary']['inspected'] ?? 0 ) > 0, 'active/no-signal report inspects rows'); + $assert(true, isset($active_report['evidence']['probe_cache']['default_ref']['misses']), 'active/no-signal report exposes probe cache stats'); $active_rows = array(); foreach ( (array) ( $active_report['rows'] ?? array() ) as $row ) { $active_rows[ $row['handle'] ?? '' ] = $row; From f95666445f45bcada351fc0f80676e1c24ac5dcb Mon Sep 17 00:00:00 2001 From: Chris Huber Date: Sun, 7 Jun 2026 09:20:58 -0400 Subject: [PATCH 2/2] chore: align active probe cache formatting --- inc/Workspace/Workspace.php | 26 +++++++++++++++------ tests/smoke-worktree-metadata-reconcile.php | 5 +++- 2 files changed, 23 insertions(+), 8 deletions(-) diff --git a/inc/Workspace/Workspace.php b/inc/Workspace/Workspace.php index 7e7a6b88..b200f6dc 100644 --- a/inc/Workspace/Workspace.php +++ b/inc/Workspace/Workspace.php @@ -1661,10 +1661,22 @@ public function worktree_active_no_signal_report( array $opts = array() ): array 'remote_tracking' => array(), 'commits_outside_default' => array(), 'stats' => array( - 'default_ref' => array( 'hits' => 0, 'misses' => 0 ), - 'github_slug' => array( 'hits' => 0, 'misses' => 0 ), - 'remote_tracking' => array( 'hits' => 0, 'misses' => 0 ), - 'commits_outside_default' => array( 'hits' => 0, 'misses' => 0 ), + 'default_ref' => array( + 'hits' => 0, + 'misses' => 0, + ), + 'github_slug' => array( + 'hits' => 0, + 'misses' => 0, + ), + 'remote_tracking' => array( + 'hits' => 0, + 'misses' => 0, + ), + 'commits_outside_default' => array( + 'hits' => 0, + 'misses' => 0, + ), ), ); $rows = array(); @@ -2826,7 +2838,7 @@ private function cached_active_no_signal_remote_tracking_probe( string $primary_ } $this->record_active_no_signal_probe_cache_stat($probe_cache, 'remote_tracking', false); - $result = $this->run_git($primary_path, sprintf('rev-parse --verify --quiet %s', escapeshellarg($remote_ref)), self::CLEANUP_GIT_PROBE_TIMEOUT); + $result = $this->run_git($primary_path, sprintf('rev-parse --verify --quiet %s', escapeshellarg($remote_ref)), self::CLEANUP_GIT_PROBE_TIMEOUT); $probe_cache['remote_tracking'][ $key ] = $result; return $result; } @@ -2867,7 +2879,7 @@ private function cached_active_no_signal_commits_outside_default_probe( string $ } $this->record_active_no_signal_probe_cache_stat($probe_cache, 'commits_outside_default', false); - $result = $this->run_git( + $result = $this->run_git( $primary_path, sprintf('rev-list --count %s..%s', escapeshellarg($default_ref), escapeshellarg('refs/heads/' . $branch)), self::CLEANUP_GIT_PROBE_TIMEOUT @@ -2890,7 +2902,7 @@ private function cached_active_no_signal_github_slug_probe( string $primary_path } $this->record_active_no_signal_probe_cache_stat($probe_cache, 'github_slug', false); - $result = $this->resolve_github_slug($primary_path); + $result = $this->resolve_github_slug($primary_path); $probe_cache['github_slug'][ $primary_path ] = $result; return $result; } diff --git a/tests/smoke-worktree-metadata-reconcile.php b/tests/smoke-worktree-metadata-reconcile.php index 2de843bc..e00fc40c 100644 --- a/tests/smoke-worktree-metadata-reconcile.php +++ b/tests/smoke-worktree-metadata-reconcile.php @@ -528,7 +528,10 @@ function () use ( $tmp ) { $default_ref_probe_cache = array( 'default_ref' => array(), 'stats' => array( - 'default_ref' => array( 'hits' => 0, 'misses' => 0 ), + 'default_ref' => array( + 'hits' => 0, + 'misses' => 0, + ), ), ); $first_default_ref = $default_ref_cache_reflection->invokeArgs($ws, array( $primary, &$default_ref_probe_cache ));