Skip to content

Commit a68e143

Browse files
authored
Merge pull request #133 from githubnext/copilot/fix-perf-benchmark-autoloop
Fix autoloop creating multiple PRs by preserving exact branch names
2 parents 020828e + 72d9346 commit a68e143

15 files changed

Lines changed: 177 additions & 61 deletions

.github/workflows/autoloop.lock.yml

Lines changed: 20 additions & 20 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

.github/workflows/autoloop.md

Lines changed: 56 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ safe-outputs:
4545
title-prefix: "[Autoloop] "
4646
labels: [automation, autoloop]
4747
protected-files: fallback-to-issue
48+
preserve-branch-name: true
4849
max: 1
4950
push-to-pull-request-branch:
5051
target: "*"
@@ -434,10 +435,26 @@ steps:
434435
# Look up existing PR for the selected program's canonical branch
435436
existing_pr = None
436437
head_branch = None
438+
439+
def verify_pr_is_open(pr_number):
440+
"""Check if a PR is still open via the GitHub API. Returns True if open."""
441+
try:
442+
verify_url = f"https://api.github.com/repos/{repo}/pulls/{pr_number}"
443+
verify_req = urllib.request.Request(verify_url, headers={
444+
"Authorization": f"token {github_token}",
445+
"Accept": "application/vnd.github.v3+json",
446+
})
447+
with urllib.request.urlopen(verify_req, timeout=30) as verify_resp:
448+
pr_data = json.loads(verify_resp.read().decode())
449+
return pr_data.get("state") == "open"
450+
except Exception:
451+
return True # If we can't verify, assume it's open (best effort)
452+
437453
if selected:
438454
head_branch = f"autoloop/{selected}"
439455
owner = repo.split("/")[0] if "/" in repo else ""
440456
if owner:
457+
# Strategy 1: exact branch match (works when branch has no framework suffix)
441458
try:
442459
pr_api_url = (
443460
f"https://api.github.com/repos/{repo}/pulls"
@@ -451,22 +468,54 @@ steps:
451468
open_prs = json.loads(pr_resp.read().decode())
452469
if open_prs:
453470
existing_pr = open_prs[0]["number"]
454-
print(f" Found existing PR #{existing_pr} for branch {head_branch}")
455-
else:
456-
print(f" No existing PR found for branch {head_branch}")
471+
print(f" Found existing PR #{existing_pr} for exact branch {head_branch}")
457472
except Exception as e:
458-
print(f" Warning: could not check for existing PRs: {e}")
473+
print(f" Warning: could not check for existing PRs by exact branch: {e}")
474+
475+
# Strategy 2: search by title and branch prefix (catches framework-generated
476+
# hash suffixes like autoloop/name-a1b2c3d4e5f6g7h8 created by create-pull-request)
477+
if existing_pr is None:
478+
try:
479+
title_marker = f"[Autoloop: {selected}]"
480+
branch_prefix = head_branch # e.g. autoloop/perf-comparison
481+
list_url = (
482+
f"https://api.github.com/repos/{repo}/pulls"
483+
f"?state=open&per_page=100&sort=created&direction=desc"
484+
)
485+
list_req = urllib.request.Request(list_url, headers={
486+
"Authorization": f"token {github_token}",
487+
"Accept": "application/vnd.github.v3+json",
488+
})
489+
with urllib.request.urlopen(list_req, timeout=30) as list_resp:
490+
all_open_prs = json.loads(list_resp.read().decode())
491+
# Match branch names: exact canonical name or canonical + framework hash suffix
492+
branch_pattern = re.compile(r'^' + re.escape(branch_prefix) + r'(-[0-9a-f]{16})?$')
493+
for pr in all_open_prs:
494+
pr_title = pr.get("title", "")
495+
pr_head_ref = pr.get("head", {}).get("ref", "")
496+
if title_marker in pr_title or branch_pattern.match(pr_head_ref):
497+
existing_pr = pr["number"]
498+
print(f" Found existing PR #{existing_pr} by title/branch-prefix (branch: {pr_head_ref})")
499+
break
500+
if existing_pr is None:
501+
print(f" No existing PR found for program {selected}")
502+
except Exception as e:
503+
print(f" Warning: could not search for existing PRs by title/prefix: {e}")
459504
else:
460505
print(f" Warning: could not parse owner from GITHUB_REPOSITORY='{repo}'")
461506
462-
# Also check the state file for a recorded PR number as fallback
507+
# Strategy 3: check the state file for a recorded PR number as fallback
463508
if existing_pr is None:
464509
state = read_program_state(selected)
465510
pr_field = state.get("pr") or ""
466511
pr_match = re.match(r'^#?(\d+)$', pr_field.strip())
467512
if pr_match:
468-
existing_pr = int(pr_match.group(1))
469-
print(f" Found PR #{existing_pr} from state file for {selected}")
513+
pr_num = int(pr_match.group(1))
514+
if verify_pr_is_open(pr_num):
515+
existing_pr = pr_num
516+
print(f" Found open PR #{existing_pr} from state file for {selected}")
517+
else:
518+
print(f" PR #{pr_num} from state file is no longer open — ignoring")
470519
471520
result = {
472521
"selected": selected,

biome.json

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,14 @@
77
},
88
"files": {
99
"ignoreUnknown": false,
10-
"ignore": ["dist/**", "node_modules/**", "*.d.ts", "playground/**/*.js", "playground/serve.ts"]
10+
"ignore": [
11+
"dist/**",
12+
"node_modules/**",
13+
"*.d.ts",
14+
"playground/**/*.js",
15+
"playground/serve.ts",
16+
"benchmarks/**"
17+
]
1118
},
1219
"formatter": {
1320
"enabled": true,

src/core/attrs.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -227,7 +227,9 @@ export function setAttr(obj: object, key: string, value: unknown): void {
227227
*/
228228
export function deleteAttr(obj: object, key: string): void {
229229
const existing = registry.get(obj);
230-
if (existing === undefined) return;
230+
if (existing === undefined) {
231+
return;
232+
}
231233
const { [key]: _removed, ...rest } = existing;
232234
if (Object.keys(rest).length === 0) {
233235
registry.delete(obj);

src/core/frame.ts

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -102,10 +102,14 @@ export class DataFrame {
102102
* @param columns - Ordered map of column name → Series (all same length and index).
103103
* @param index - Row index (must match each Series' length).
104104
*/
105-
constructor(columns: ReadonlyMap<string, Series<Scalar>>, index: Index<Label>) {
105+
constructor(
106+
columns: ReadonlyMap<string, Series<Scalar>>,
107+
index: Index<Label>,
108+
columnNames?: readonly string[],
109+
) {
106110
this._columns = columns;
107111
this.index = index;
108-
this.columns = new Index<string>([...columns.keys()]);
112+
this.columns = new Index<string>(columnNames ?? [...columns.keys()]);
109113
}
110114

111115
/**
@@ -208,7 +212,7 @@ export class DataFrame {
208212

209213
/** `[nRows, nCols]` — mirrors `pandas.DataFrame.shape`. */
210214
get shape(): [number, number] {
211-
return [this.index.size, this._columns.size];
215+
return [this.index.size, this.columns.size];
212216
}
213217

214218
/** Always `2`. */
@@ -218,12 +222,12 @@ export class DataFrame {
218222

219223
/** Total number of cells (`nRows * nCols`). */
220224
get size(): number {
221-
return this.index.size * this._columns.size;
225+
return this.index.size * this.columns.size;
222226
}
223227

224228
/** `true` when the DataFrame has no rows or no columns. */
225229
get empty(): boolean {
226-
return this.index.size === 0 || this._columns.size === 0;
230+
return this.index.size === 0 || this.columns.size === 0;
227231
}
228232

229233
// ─── column access ────────────────────────────────────────────────────────

src/core/insert_pop.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -82,23 +82,28 @@ export function insertColumn(
8282
}
8383

8484
// Rebuild the column map, inserting the new column at position `loc`.
85+
const colNames: string[] = [];
8586
const colMap = new Map<string, Series<Scalar>>();
8687
let idx = 0;
8788

8889
for (const colName of df.columns.values) {
8990
if (idx === loc) {
90-
colMap.set(column, series);
91+
colNames.push(column);
9192
}
93+
colNames.push(colName);
9294
colMap.set(colName, df.col(colName));
9395
idx++;
9496
}
9597

9698
// Handle insertion at the end (loc === nCols).
9799
if (loc === nCols) {
98-
colMap.set(column, series);
100+
colNames.push(column);
99101
}
100102

101-
return new DataFrame(colMap, df.index);
103+
// Always add the new column data to the map (last-wins for duplicate names).
104+
colMap.set(column, series);
105+
106+
return new DataFrame(colMap, df.index, colNames);
102107
}
103108

104109
// ─── popColumn ────────────────────────────────────────────────────────────────

src/reshape/wide_to_long.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,7 @@ function collectSuffixes(
100100
// Sort numerically when both look like integers, otherwise lexicographically.
101101
const na = Number(a);
102102
const nb = Number(b);
103-
if (!Number.isNaN(na) && !Number.isNaN(nb)) {
103+
if (!(Number.isNaN(na) || Number.isNaN(nb))) {
104104
return na - nb;
105105
}
106106
return a < b ? -1 : a > b ? 1 : 0;

src/stats/categorical_ops.ts

Lines changed: 41 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,9 @@ export function catFromCodes(
108108
const { ordered = false, name = null } = opts;
109109
const cats = uniqueKeys(categories);
110110
const values: Scalar[] = codes.map((code) => {
111-
if (code === -1) return null;
111+
if (code === -1) {
112+
return null;
113+
}
112114
if (code < -1 || code >= cats.length) {
113115
throw new RangeError(`catFromCodes: code ${code} is out of range [0, ${cats.length - 1}]`);
114116
}
@@ -210,9 +212,13 @@ export function catDiffCategories(a: CatSeriesLike, b: CatSeriesLike): CatSeries
210212
export function catEqualCategories(a: CatSeriesLike, b: CatSeriesLike): boolean {
211213
const aSet = new Set((a.cat.categories.values as Scalar[]).map(String));
212214
const bSet = new Set((b.cat.categories.values as Scalar[]).map(String));
213-
if (aSet.size !== bSet.size) return false;
215+
if (aSet.size !== bSet.size) {
216+
return false;
217+
}
214218
for (const c of aSet) {
215-
if (!bSet.has(c)) return false;
219+
if (!bSet.has(c)) {
220+
return false;
221+
}
216222
}
217223
return true;
218224
}
@@ -243,12 +249,16 @@ export function catSortByFreq(
243249
const { ascending = false } = opts;
244250
const cats = series.cat.categories.values as Scalar[];
245251
const freq = new Map<string, number>();
246-
for (const c of cats) freq.set(String(c), 0);
252+
for (const c of cats) {
253+
freq.set(String(c), 0);
254+
}
247255
for (const v of series.values) {
248256
if (!isMissing(v)) {
249257
const k = String(v);
250258
const prev = freq.get(k);
251-
if (prev !== undefined) freq.set(k, prev + 1);
259+
if (prev !== undefined) {
260+
freq.set(k, prev + 1);
261+
}
252262
}
253263
}
254264
const sorted = [...cats].sort((a, b) => {
@@ -306,7 +316,9 @@ export function catToOrdinal(series: CatSeriesLike, order: readonly Scalar[]): C
306316
export function catFreqTable(series: CatSeriesLike): Record<string, number> {
307317
const cats = series.cat.categories.values as Scalar[];
308318
const freq: Record<string, number> = {};
309-
for (const c of cats) freq[String(c)] = 0;
319+
for (const c of cats) {
320+
freq[String(c)] = 0;
321+
}
310322
for (const v of series.values) {
311323
if (!isMissing(v)) {
312324
const k = String(v);
@@ -358,7 +370,9 @@ export function catCrossTab(
358370
const counts = new Map<string, Map<string, number>>();
359371
for (const r of rowCats) {
360372
const row = new Map<string, number>();
361-
for (const c of colCats) row.set(String(c), 0);
373+
for (const c of colCats) {
374+
row.set(String(c), 0);
375+
}
362376
counts.set(String(r), row);
363377
}
364378

@@ -368,18 +382,26 @@ export function catCrossTab(
368382
for (let i = 0; i < n; i++) {
369383
const av = aVals[i];
370384
const bv = bVals[i];
371-
if (isMissing(av) || isMissing(bv)) continue;
385+
if (isMissing(av) || isMissing(bv)) {
386+
continue;
387+
}
372388
const row = counts.get(String(av));
373-
if (row === undefined) continue;
389+
if (row === undefined) {
390+
continue;
391+
}
374392
const prev = row.get(String(bv));
375-
if (prev !== undefined) row.set(String(bv), prev + 1);
393+
if (prev !== undefined) {
394+
row.set(String(bv), prev + 1);
395+
}
376396
}
377397

378398
// Compute total for normalization
379399
let total = 0;
380400
if (normalize) {
381401
for (const row of counts.values()) {
382-
for (const v of row.values()) total += v;
402+
for (const v of row.values()) {
403+
total += v;
404+
}
383405
}
384406
}
385407

@@ -399,7 +421,11 @@ export function catCrossTab(
399421
const rowTotals: Scalar[] = rowCats.map((r) => {
400422
let sum = 0;
401423
const row = counts.get(String(r));
402-
if (row) for (const v of row.values()) sum += v;
424+
if (row) {
425+
for (const v of row.values()) {
426+
sum += v;
427+
}
428+
}
403429
return normalize && total > 0 ? sum / total : sum;
404430
});
405431
data[marginsName] = rowTotals;
@@ -430,7 +456,9 @@ export function catCrossTab(
430456
// Ensure all column arrays have the same length
431457
for (const col of allCols) {
432458
const arr = data[col];
433-
if (arr === undefined) data[col] = rowLabels.map(() => 0);
459+
if (arr === undefined) {
460+
data[col] = rowLabels.map(() => 0);
461+
}
434462
}
435463
}
436464

src/stats/window_extended.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,7 @@ function applyWindow(
113113
minN: number,
114114
agg: (nums: number[], n: number) => Scalar,
115115
): SeriesLike {
116-
const { values, index, name } = series;
116+
const { values, name } = series;
117117
const n = values.length;
118118
const minPeriods = opts.minPeriods ?? window;
119119
const effectiveMin = Math.max(minN, minPeriods);

src/window/rolling_apply.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -82,8 +82,12 @@ function validNums(slice: readonly Scalar[]): number[] {
8282
/** Convert a raw window slice to `null`-substituted numeric array. */
8383
function rawWindow(slice: readonly Scalar[]): (number | null)[] {
8484
return slice.map((v): number | null => {
85-
if (isMissing(v)) return null;
86-
if (typeof v === "number") return v;
85+
if (isMissing(v)) {
86+
return null;
87+
}
88+
if (typeof v === "number") {
89+
return v;
90+
}
8791
return null;
8892
});
8993
}

0 commit comments

Comments
 (0)