Skip to content

Commit c779193

Browse files
authored
Merge pull request #587 from Extra-Chill/fix/active-no-signal-probe-cache
Cache active worktree classifier probes
2 parents e602a43 + f956664 commit c779193

2 files changed

Lines changed: 163 additions & 18 deletions

File tree

inc/Workspace/Workspace.php

Lines changed: 147 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1655,6 +1655,30 @@ public function worktree_active_no_signal_report( array $opts = array() ): array
16551655
$page = array_slice($active, $offset, $limit);
16561656

16571657
$github_cache = array();
1658+
$probe_cache = array(
1659+
'default_ref' => array(),
1660+
'github_slug' => array(),
1661+
'remote_tracking' => array(),
1662+
'commits_outside_default' => array(),
1663+
'stats' => array(
1664+
'default_ref' => array(
1665+
'hits' => 0,
1666+
'misses' => 0,
1667+
),
1668+
'github_slug' => array(
1669+
'hits' => 0,
1670+
'misses' => 0,
1671+
),
1672+
'remote_tracking' => array(
1673+
'hits' => 0,
1674+
'misses' => 0,
1675+
),
1676+
'commits_outside_default' => array(
1677+
'hits' => 0,
1678+
'misses' => 0,
1679+
),
1680+
),
1681+
);
16581682
$rows = array();
16591683
$summary = array(
16601684
'total_active_no_signal' => $total,
@@ -1674,7 +1698,7 @@ public function worktree_active_no_signal_report( array $opts = array() ): array
16741698
break;
16751699
}
16761700
$row_started = microtime(true);
1677-
$evidence = $this->build_active_no_signal_evidence_row($row, $github_cache);
1701+
$evidence = $this->build_active_no_signal_evidence_row($row, $github_cache, $probe_cache);
16781702
$evidence['elapsed_ms'] = (int) round(( microtime(true) - $row_started ) * 1000);
16791703
$rows[] = $evidence;
16801704
++$summary['inspected'];
@@ -1716,9 +1740,10 @@ public function worktree_active_no_signal_report( array $opts = array() ): array
17161740
'summary' => array_merge($summary, array( 'slow_rows' => $this->summarize_slow_worktree_rows($rows) )),
17171741
'pagination' => $pagination,
17181742
'evidence' => array(
1719-
'scope' => 'review-only active_no_signal worktree lifecycle evidence',
1720-
'safety' => 'No worktrees or remote branches are deleted. Dirty and unpushed probes are evidence only.',
1721-
'budget' => null === $budget_context ? null : $this->summarize_worktree_loop_budget_context($budget_context, $budget_stopped),
1743+
'scope' => 'review-only active_no_signal worktree lifecycle evidence',
1744+
'safety' => 'No worktrees or remote branches are deleted. Dirty and unpushed probes are evidence only.',
1745+
'budget' => null === $budget_context ? null : $this->summarize_worktree_loop_budget_context($budget_context, $budget_stopped),
1746+
'probe_cache' => $probe_cache['stats'],
17221747
),
17231748
);
17241749
}
@@ -2672,9 +2697,10 @@ private function build_active_no_signal_finalized_apply_skip( array $row, string
26722697
*
26732698
* @param array<string,mixed> $row Inventory skip row.
26742699
* @param array<string,mixed> $github_cache Run-local GitHub cache.
2700+
* @param array<string,mixed> $probe_cache Run-local git probe cache.
26752701
* @return array<string,mixed>
26762702
*/
2677-
private function build_active_no_signal_evidence_row( array $row, array &$github_cache ): array {
2703+
private function build_active_no_signal_evidence_row( array $row, array &$github_cache, array &$probe_cache ): array {
26782704
$handle = (string) ( $row['handle'] ?? '' );
26792705
$repo = (string) ( $row['repo'] ?? '' );
26802706
$branch = (string) ( $row['branch'] ?? '' );
@@ -2684,11 +2710,16 @@ private function build_active_no_signal_evidence_row( array $row, array &$github
26842710
$metadata = is_array($row['metadata'] ?? null) ? $row['metadata'] : array();
26852711
$branch_probe = null;
26862712
if ( '' !== $path && is_dir($path) ) {
2687-
$branch_probe = $this->run_git($path, 'branch --show-current', self::CLEANUP_GIT_PROBE_TIMEOUT);
2688-
if ( ! is_wp_error($branch_probe) && ! $this->is_git_timeout_error($branch_probe) ) {
2689-
$actual_branch = trim( (string) ( $branch_probe['output'] ?? '' ) );
2690-
if ( '' !== $actual_branch ) {
2691-
$branch = $actual_branch;
2713+
$head_branch = $this->resolve_worktree_branch_from_head_file($path);
2714+
if ( null !== $head_branch && '' !== $head_branch ) {
2715+
$branch = $head_branch;
2716+
} else {
2717+
$branch_probe = $this->run_git($path, 'branch --show-current', self::CLEANUP_GIT_PROBE_TIMEOUT);
2718+
if ( ! is_wp_error($branch_probe) && ! $this->is_git_timeout_error($branch_probe) ) {
2719+
$actual_branch = trim( (string) ( $branch_probe['output'] ?? '' ) );
2720+
if ( '' !== $actual_branch ) {
2721+
$branch = $actual_branch;
2722+
}
26922723
}
26932724
}
26942725
}
@@ -2749,18 +2780,16 @@ private function build_active_no_signal_evidence_row( array $row, array &$github
27492780
}
27502781

27512782
$remote_ref = 'refs/remotes/origin/' . $branch;
2752-
$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));
2783+
$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));
27532784
$out['remote_tracking'] = ! is_wp_error($remote) && ! $this->is_git_timeout_error($remote);
27542785

2755-
$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));
2786+
$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));
27562787
if ( is_string($default_ref) && '' !== $default_ref ) {
27572788
$out['default_ref'] = $default_ref;
27582789
$outside = $this->time_worktree_probe(
2759-
$out['probe_timings_ms'], 'commits_outside_default', fn() => $this->run_git(
2760-
$primary_path,
2761-
sprintf('rev-list --count %s..%s', escapeshellarg($default_ref), escapeshellarg('refs/heads/' . $branch)),
2762-
self::CLEANUP_GIT_PROBE_TIMEOUT
2763-
)
2790+
$out['probe_timings_ms'],
2791+
'commits_outside_default',
2792+
fn() => $this->cached_active_no_signal_commits_outside_default_probe($primary_path, $default_ref, $branch, $probe_cache)
27642793
);
27652794
if ( ! is_wp_error($outside) && ! $this->is_git_timeout_error($outside) ) {
27662795
$out['commits_outside_default'] = (int) trim( (string) ( $outside['output'] ?? '' ));
@@ -2776,7 +2805,7 @@ private function build_active_no_signal_evidence_row( array $row, array &$github
27762805
if ( (int) ( $out['dirty'] ?? 0 ) > 0 || (int) ( $out['unpushed'] ?? 0 ) > 0 ) {
27772806
$out['pr_lookup_skipped'] = 'dirty_or_unpushed_rows_are_always_manual_review';
27782807
} else {
2779-
$slug = $this->time_worktree_probe($out['probe_timings_ms'], 'github_slug', fn() => $this->resolve_github_slug($primary_path));
2808+
$slug = $this->time_worktree_probe($out['probe_timings_ms'], 'github_slug', fn() => $this->cached_active_no_signal_github_slug_probe($primary_path, $probe_cache));
27802809
if ( null !== $slug ) {
27812810
$pr = $this->time_worktree_probe($out['probe_timings_ms'], 'github_pr_lookup', fn() => $this->find_pr_for_branch_direct($slug, $branch, $github_cache, false));
27822811
if ( is_wp_error($pr) ) {
@@ -2793,6 +2822,106 @@ private function build_active_no_signal_evidence_row( array $row, array &$github
27932822
return $out;
27942823
}
27952824

2825+
/**
2826+
* Cache a remote-tracking ref existence probe for an active/no-signal report run.
2827+
*
2828+
* @param string $primary_path Primary checkout path.
2829+
* @param string $remote_ref Fully-qualified remote-tracking ref.
2830+
* @param array<string,mixed> $probe_cache Run-local git probe cache.
2831+
* @return array<string,mixed>|\WP_Error Git result or timeout/error.
2832+
*/
2833+
private function cached_active_no_signal_remote_tracking_probe( string $primary_path, string $remote_ref, array &$probe_cache ): array|\WP_Error {
2834+
$key = $primary_path . '#' . $remote_ref;
2835+
if ( array_key_exists($key, $probe_cache['remote_tracking'] ?? array()) ) {
2836+
$this->record_active_no_signal_probe_cache_stat($probe_cache, 'remote_tracking', true);
2837+
return $probe_cache['remote_tracking'][ $key ];
2838+
}
2839+
2840+
$this->record_active_no_signal_probe_cache_stat($probe_cache, 'remote_tracking', false);
2841+
$result = $this->run_git($primary_path, sprintf('rev-parse --verify --quiet %s', escapeshellarg($remote_ref)), self::CLEANUP_GIT_PROBE_TIMEOUT);
2842+
$probe_cache['remote_tracking'][ $key ] = $result;
2843+
return $result;
2844+
}
2845+
2846+
/**
2847+
* Cache the remote default ref for an active/no-signal report run.
2848+
*
2849+
* @param string $primary_path Primary checkout path.
2850+
* @param array<string,mixed> $probe_cache Run-local git probe cache.
2851+
* @return string|\WP_Error|null Fully-qualified remote default ref, timeout/error, or null.
2852+
*/
2853+
private function cached_active_no_signal_default_ref_probe( string $primary_path, array &$probe_cache ): string|\WP_Error|null {
2854+
if ( array_key_exists($primary_path, $probe_cache['default_ref'] ?? array()) ) {
2855+
$this->record_active_no_signal_probe_cache_stat($probe_cache, 'default_ref', true);
2856+
return $probe_cache['default_ref'][ $primary_path ];
2857+
}
2858+
2859+
$this->record_active_no_signal_probe_cache_stat($probe_cache, 'default_ref', false);
2860+
$result = $this->resolve_remote_default_ref($primary_path, self::CLEANUP_GIT_PROBE_TIMEOUT);
2861+
$probe_cache['default_ref'][ $primary_path ] = $result;
2862+
return $result;
2863+
}
2864+
2865+
/**
2866+
* Cache commits-outside-default probes for an active/no-signal report run.
2867+
*
2868+
* @param string $primary_path Primary checkout path.
2869+
* @param string $default_ref Fully-qualified remote default ref.
2870+
* @param string $branch Local branch name.
2871+
* @param array<string,mixed> $probe_cache Run-local git probe cache.
2872+
* @return array<string,mixed>|\WP_Error Git result or timeout/error.
2873+
*/
2874+
private function cached_active_no_signal_commits_outside_default_probe( string $primary_path, string $default_ref, string $branch, array &$probe_cache ): array|\WP_Error {
2875+
$key = $primary_path . '#' . $default_ref . '#' . $branch;
2876+
if ( array_key_exists($key, $probe_cache['commits_outside_default'] ?? array()) ) {
2877+
$this->record_active_no_signal_probe_cache_stat($probe_cache, 'commits_outside_default', true);
2878+
return $probe_cache['commits_outside_default'][ $key ];
2879+
}
2880+
2881+
$this->record_active_no_signal_probe_cache_stat($probe_cache, 'commits_outside_default', false);
2882+
$result = $this->run_git(
2883+
$primary_path,
2884+
sprintf('rev-list --count %s..%s', escapeshellarg($default_ref), escapeshellarg('refs/heads/' . $branch)),
2885+
self::CLEANUP_GIT_PROBE_TIMEOUT
2886+
);
2887+
$probe_cache['commits_outside_default'][ $key ] = $result;
2888+
return $result;
2889+
}
2890+
2891+
/**
2892+
* Cache the GitHub slug for an active/no-signal report run.
2893+
*
2894+
* @param string $primary_path Primary checkout path.
2895+
* @param array<string,mixed> $probe_cache Run-local git probe cache.
2896+
* @return string|null owner/repo or null when origin is not GitHub.
2897+
*/
2898+
private function cached_active_no_signal_github_slug_probe( string $primary_path, array &$probe_cache ): ?string {
2899+
if ( array_key_exists($primary_path, $probe_cache['github_slug'] ?? array()) ) {
2900+
$this->record_active_no_signal_probe_cache_stat($probe_cache, 'github_slug', true);
2901+
return $probe_cache['github_slug'][ $primary_path ];
2902+
}
2903+
2904+
$this->record_active_no_signal_probe_cache_stat($probe_cache, 'github_slug', false);
2905+
$result = $this->resolve_github_slug($primary_path);
2906+
$probe_cache['github_slug'][ $primary_path ] = $result;
2907+
return $result;
2908+
}
2909+
2910+
/**
2911+
* Record active/no-signal probe cache hit/miss counts.
2912+
*
2913+
* @param array<string,mixed> $probe_cache Run-local git probe cache.
2914+
* @param string $bucket Probe cache bucket.
2915+
* @param bool $hit Whether the lookup was a cache hit.
2916+
*/
2917+
private function record_active_no_signal_probe_cache_stat( array &$probe_cache, string $bucket, bool $hit ): void {
2918+
$field = $hit ? 'hits' : 'misses';
2919+
if ( ! isset($probe_cache['stats'][ $bucket ][ $field ]) ) {
2920+
$probe_cache['stats'][ $bucket ][ $field ] = 0;
2921+
}
2922+
++$probe_cache['stats'][ $bucket ][ $field ];
2923+
}
2924+
27962925
/**
27972926
* Build patch-equivalence evidence for clean active/no-signal worktrees.
27982927
*

tests/smoke-worktree-metadata-reconcile.php

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -524,6 +524,21 @@ function () use ( $tmp ) {
524524
)
525525
);
526526
$ws = new \DataMachineCode\Workspace\Workspace();
527+
$default_ref_cache_reflection = new \ReflectionMethod($ws, 'cached_active_no_signal_default_ref_probe');
528+
$default_ref_probe_cache = array(
529+
'default_ref' => array(),
530+
'stats' => array(
531+
'default_ref' => array(
532+
'hits' => 0,
533+
'misses' => 0,
534+
),
535+
),
536+
);
537+
$first_default_ref = $default_ref_cache_reflection->invokeArgs($ws, array( $primary, &$default_ref_probe_cache ));
538+
$second_default_ref = $default_ref_cache_reflection->invokeArgs($ws, array( $primary, &$default_ref_probe_cache ));
539+
$assert($first_default_ref, $second_default_ref, 'active/no-signal default ref cache returns stable cached values');
540+
$assert(1, (int) ( $default_ref_probe_cache['stats']['default_ref']['hits'] ?? 0 ), 'active/no-signal default ref cache records one hit after reuse');
541+
$assert(1, (int) ( $default_ref_probe_cache['stats']['default_ref']['misses'] ?? 0 ), 'active/no-signal default ref cache records one miss before reuse');
527542
$lookup_reflection = new \ReflectionMethod($ws, 'find_closed_pr_for_branch');
528543
$lookup_cache = array( 'acme/demo' => array() );
529544
$old_pr = $lookup_reflection->invokeArgs($ws, array( 'acme/demo', 'old-merged-branch', &$lookup_cache ));
@@ -537,6 +552,7 @@ function () use ( $tmp ) {
537552
$assert(true, ! is_wp_error($active_report) && ( $active_report['success'] ?? false ), 'active/no-signal report succeeds');
538553
$assert(true, (bool) ( $active_report['review_only'] ?? false ), 'active/no-signal report is review-only');
539554
$assert(true, (int) ( $active_report['summary']['inspected'] ?? 0 ) > 0, 'active/no-signal report inspects rows');
555+
$assert(true, isset($active_report['evidence']['probe_cache']['default_ref']['misses']), 'active/no-signal report exposes probe cache stats');
540556
$active_rows = array();
541557
foreach ( (array) ( $active_report['rows'] ?? array() ) as $row ) {
542558
$active_rows[ $row['handle'] ?? '' ] = $row;

0 commit comments

Comments
 (0)