Skip to content

Commit 7b7cc99

Browse files
committed
Implement a real js test looper
1 parent 3ed6c1d commit 7b7cc99

2 files changed

Lines changed: 408 additions & 106 deletions

File tree

codeflash/languages/javascript/test_runner.py

Lines changed: 55 additions & 106 deletions
Original file line numberDiff line numberDiff line change
@@ -419,27 +419,29 @@ def run_jest_benchmarking_tests(
419419
target_duration_ms: int = 10_000, # 10 seconds for benchmarking tests
420420
stability_check: bool = True,
421421
) -> tuple[Path, subprocess.CompletedProcess]:
422-
"""Run Jest benchmarking tests with session-level looping for stable measurements.
422+
"""Run Jest benchmarking tests with in-process session-level looping.
423+
424+
Uses a custom Jest runner (codeflash/loop-runner) to loop all tests
425+
within a single Jest process, eliminating process startup overhead.
423426
424427
This matches Python's pytest_plugin behavior:
425-
- Run Jest multiple times (like pytest loops the session)
426-
- Each Jest run executes each test once
427-
- Timing data is collected per Jest run
428-
- Stability is checked across all Jest runs (session-level)
428+
- All tests are run multiple times within a single Jest process
429+
- Timing data is collected per iteration
430+
- Stability is checked within the runner
429431
430432
Args:
431433
test_paths: TestFiles object containing test file information.
432434
test_env: Environment variables for the test run.
433435
cwd: Working directory for running tests.
434-
timeout: Optional timeout in seconds per Jest invocation.
436+
timeout: Optional timeout in seconds for the entire benchmark run.
435437
project_root: JavaScript project root (directory containing package.json).
436-
min_loops: Minimum number of Jest runs (session loops).
437-
max_loops: Maximum number of Jest runs (session loops).
438+
min_loops: Minimum number of loop iterations.
439+
max_loops: Maximum number of loop iterations.
438440
target_duration_ms: Target TOTAL duration in milliseconds for all loops.
439441
stability_check: Whether to enable stability-based early stopping.
440442
441443
Returns:
442-
Tuple of (result_file_path, subprocess_result with combined stdout from all runs).
444+
Tuple of (result_file_path, subprocess_result with stdout from all iterations).
443445
444446
"""
445447
result_file_path = get_run_tmp_file(Path("jest_perf_results.xml"))
@@ -458,8 +460,16 @@ def run_jest_benchmarking_tests(
458460
# Ensure the codeflash npm package is installed
459461
_ensure_runtime_files(effective_cwd)
460462

461-
# Build Jest command for performance tests
462-
jest_cmd = ["npx", "jest", "--reporters=default", "--reporters=jest-junit", "--runInBand", "--forceExit"]
463+
# Build Jest command for performance tests with custom loop runner
464+
jest_cmd = [
465+
"npx",
466+
"jest",
467+
"--reporters=default",
468+
"--reporters=jest-junit",
469+
"--runInBand", # Ensure serial execution even though runner enforces it
470+
"--forceExit",
471+
"--runner=codeflash/loop-runner", # Use custom loop runner for in-process looping
472+
]
463473

464474
if test_files:
465475
jest_cmd.append("--runTestsByPath")
@@ -483,118 +493,57 @@ def run_jest_benchmarking_tests(
483493
jest_env["CODEFLASH_MODE"] = "performance"
484494
jest_env["CODEFLASH_RANDOM_SEED"] = "42"
485495

496+
# Loop runner configuration (passed via environment variables)
497+
jest_env["CODEFLASH_LOOP_COUNT"] = str(max_loops)
498+
jest_env["CODEFLASH_MIN_LOOPS"] = str(min_loops)
499+
jest_env["CODEFLASH_TARGET_DURATION_MS"] = str(target_duration_ms)
500+
jest_env["CODEFLASH_STABILITY_CHECK"] = "true" if stability_check else "false"
501+
jest_env["CODEFLASH_LOOP_INDEX"] = "1" # Initial value, updated by runner
502+
486503
# Configure ESM support if project uses ES Modules
487504
_configure_esm_environment(jest_env, effective_cwd)
488505

489-
# Per-Jest-invocation timeout (not total timeout)
490-
subprocess_timeout = max(60, timeout or 60)
506+
# Total timeout for the entire benchmark run (longer than single-loop timeout)
507+
# Account for startup overhead + target duration + buffer
508+
total_timeout = max(120, (target_duration_ms // 1000) + 60, timeout or 120)
491509

492-
logger.debug(f"Running Jest benchmarking tests with session-level looping: {' '.join(jest_cmd)}")
510+
logger.debug(f"Running Jest benchmarking tests with in-process loop runner: {' '.join(jest_cmd)}")
493511
logger.debug(
494512
f"Jest benchmarking config: min_loops={min_loops}, max_loops={max_loops}, "
495513
f"target_duration={target_duration_ms}ms, stability_check={stability_check}"
496514
)
497515

498-
# Session-level looping (like Python's pytest_plugin)
499-
runtime_data_by_test: dict[str, list[int]] = {}
500-
aggregate_runtimes: list[int] = []
501-
all_stdout: list[str] = []
502-
final_result: subprocess.CompletedProcess | None = None
503516
total_start_time = time.time()
504-
loop_count = 0
505-
506-
while loop_count < max_loops:
507-
loop_count += 1
508-
509-
# Set loop index for this Jest run
510-
jest_env["CODEFLASH_LOOP_INDEX"] = str(loop_count)
511517

512-
logger.debug(f"Jest benchmarking loop {loop_count}/{max_loops}")
513-
514-
try:
515-
run_args = get_cross_platform_subprocess_run_args(
516-
cwd=effective_cwd, env=jest_env, timeout=subprocess_timeout, check=False, text=True, capture_output=True
517-
)
518-
result = subprocess.run(jest_cmd, **run_args) # noqa: PLW1510
519-
520-
# Combine stderr into stdout for timing markers
521-
stdout = result.stdout or ""
522-
if result.stderr:
523-
stdout = stdout + "\n" + result.stderr if stdout else result.stderr
524-
525-
all_stdout.append(stdout)
526-
final_result = result
527-
528-
# Parse timing data from this Jest run
529-
timing_data = _parse_timing_from_jest_output(stdout)
530-
for test_id, duration_ns in timing_data.items():
531-
runtime_data_by_test.setdefault(test_id, []).append(duration_ns)
532-
533-
# Check for test failures - stop looping if tests fail
534-
if result.returncode != 0:
535-
logger.debug(f"Jest run {loop_count} failed with returncode {result.returncode}, stopping loops")
536-
break
537-
538-
except subprocess.TimeoutExpired:
539-
logger.warning(f"Jest run {loop_count} timed out after {subprocess_timeout}s")
540-
break
541-
except FileNotFoundError:
542-
logger.error("Jest not found for benchmarking")
543-
final_result = subprocess.CompletedProcess(args=jest_cmd, returncode=-1, stdout="", stderr="Jest not found")
544-
break
545-
546-
# Check stopping conditions
547-
elapsed_seconds = time.time() - total_start_time
548-
elapsed_ms = elapsed_seconds * 1000
549-
550-
# Stop if we've reached min loops AND exceeded time limit
551-
if loop_count >= min_loops and elapsed_ms >= target_duration_ms:
552-
logger.debug(
553-
f"Stopping: reached min_loops={min_loops} and elapsed={elapsed_ms:.0f}ms >= target={target_duration_ms}ms"
554-
)
555-
break
518+
try:
519+
run_args = get_cross_platform_subprocess_run_args(
520+
cwd=effective_cwd, env=jest_env, timeout=total_timeout, check=False, text=True, capture_output=True
521+
)
522+
result = subprocess.run(jest_cmd, **run_args) # noqa: PLW1510
556523

557-
# Stability check (matches Python's pytest_plugin logic exactly)
558-
if stability_check and runtime_data_by_test:
559-
# Calculate best runtime (sum of min per test case) - matches Python
560-
best_runtime = sum(min(data) for data in runtime_data_by_test.values() if data)
561-
if best_runtime > 0:
562-
aggregate_runtimes.append(best_runtime)
563-
564-
# Estimate window size based on loop rate (matches Python's pytest_plugin)
565-
# window_size = int(STABILITY_WINDOW_SIZE * estimated_total_loops + 0.5)
566-
elapsed_ns = elapsed_seconds * 1e9
567-
if elapsed_ns > 0:
568-
rate = loop_count / elapsed_ns
569-
total_time_ns = target_duration_ms * 1e6 # Convert ms to ns
570-
estimated_total_loops = int(rate * total_time_ns)
571-
window_size = int(STABILITY_WINDOW_SIZE * estimated_total_loops + 0.5)
572-
573-
if _should_stop_stability(aggregate_runtimes, window_size, min_loops):
574-
logger.debug(f"Stopping: stability reached at loop {loop_count}")
575-
break
576-
577-
# Combine all stdout from all Jest runs
578-
combined_stdout = "\n".join(all_stdout)
524+
# Combine stderr into stdout for timing markers
525+
stdout = result.stdout or ""
526+
if result.stderr:
527+
stdout = stdout + "\n" + result.stderr if stdout else result.stderr
579528

580-
wall_clock_seconds = time.time() - total_start_time
581-
logger.debug(
582-
f"Jest benchmarking completed: {loop_count} loops in {wall_clock_seconds:.2f}s, "
583-
f"{len(runtime_data_by_test)} test cases tracked"
584-
)
529+
# Create result with combined stdout
530+
result = subprocess.CompletedProcess(
531+
args=result.args, returncode=result.returncode, stdout=stdout, stderr=""
532+
)
585533

586-
# Return combined result
587-
if final_result is None:
588-
final_result = subprocess.CompletedProcess(
589-
args=jest_cmd, returncode=-1, stdout="", stderr="No Jest runs completed"
534+
except subprocess.TimeoutExpired:
535+
logger.warning(f"Jest benchmarking timed out after {total_timeout}s")
536+
result = subprocess.CompletedProcess(
537+
args=jest_cmd, returncode=-1, stdout="", stderr="Benchmarking timed out"
590538
)
539+
except FileNotFoundError:
540+
logger.error("Jest not found for benchmarking")
541+
result = subprocess.CompletedProcess(args=jest_cmd, returncode=-1, stdout="", stderr="Jest not found")
591542

592-
# Create result with combined stdout from all runs
593-
combined_result = subprocess.CompletedProcess(
594-
args=final_result.args, returncode=final_result.returncode, stdout=combined_stdout, stderr=""
595-
)
543+
wall_clock_seconds = time.time() - total_start_time
544+
logger.debug(f"Jest benchmarking completed in {wall_clock_seconds:.2f}s")
596545

597-
return result_file_path, combined_result
546+
return result_file_path, result
598547

599548

600549
def run_jest_line_profile_tests(

0 commit comments

Comments
 (0)