Skip to content

Commit 3134a9f

Browse files
committed
First e2e working version js optimizer!!
1 parent 3a96f0a commit 3134a9f

13 files changed

Lines changed: 530 additions & 78 deletions

File tree

code_to_optimize_js/codeflash.yaml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
# Codeflash configuration for JavaScript project
2+
module-root: "."
3+
tests-root: "tests"
4+
test-framework: "jest"
5+
formatter-cmds: []

code_to_optimize_js/string_utils.js

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,13 @@
88
* @returns {string} - The reversed string
99
*/
1010
function reverseString(str) {
11-
let result = '';
12-
for (let i = str.length - 1; i >= 0; i--) {
13-
result += str[i]; // Inefficient string concatenation
11+
// Efficient O(n) implementation using a single allocation and one join
12+
const len = str.length;
13+
const result = new Array(len);
14+
for (let i = 0; i < len; i++) {
15+
result[i] = str[len - 1 - i];
1416
}
15-
return result;
17+
return result.join('');
1618
}
1719

1820
/**

codeflash/api/aiservice.py

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -101,11 +101,14 @@ def make_ai_service_request(
101101
return response
102102

103103
def _get_valid_candidates(
104-
self, optimizations_json: list[dict[str, Any]], source: OptimizedCandidateSource
104+
self,
105+
optimizations_json: list[dict[str, Any]],
106+
source: OptimizedCandidateSource,
107+
language: str = "python",
105108
) -> list[OptimizedCandidate]:
106109
candidates: list[OptimizedCandidate] = []
107110
for opt in optimizations_json:
108-
code = CodeStringsMarkdown.parse_markdown_code(opt["source_code"])
111+
code = CodeStringsMarkdown.parse_markdown_code(opt["source_code"], expected_language=language)
109112
if not code.code_strings:
110113
continue
111114
candidates.append(
@@ -199,7 +202,7 @@ def optimize_code( # noqa: D417
199202
logger.debug(f"!lsp|Generating possible optimizations took {end_time - start_time:.2f} seconds.")
200203
logger.info(f"!lsp|Received {len(optimizations_json)} optimization candidates.")
201204
console.rule()
202-
return self._get_valid_candidates(optimizations_json, OptimizedCandidateSource.OPTIMIZE)
205+
return self._get_valid_candidates(optimizations_json, OptimizedCandidateSource.OPTIMIZE, language)
203206
try:
204207
error = response.json()["error"]
205208
except Exception:
@@ -399,6 +402,7 @@ def code_repair(self, request: AIServiceCodeRepairRequest) -> OptimizedCandidate
399402
"modified_source_code": request.modified_source_code,
400403
"trace_id": request.trace_id,
401404
"test_diffs": request.test_diffs,
405+
"language": request.language,
402406
}
403407
response = self.make_ai_service_request("/code_repair", payload=payload, timeout=self.timeout)
404408
except (requests.exceptions.RequestException, TypeError) as e:
@@ -410,7 +414,9 @@ def code_repair(self, request: AIServiceCodeRepairRequest) -> OptimizedCandidate
410414
fixed_optimization = response.json()
411415
console.rule()
412416

413-
valid_candidates = self._get_valid_candidates([fixed_optimization], OptimizedCandidateSource.REPAIR)
417+
valid_candidates = self._get_valid_candidates(
418+
[fixed_optimization], OptimizedCandidateSource.REPAIR, request.language
419+
)
414420
if not valid_candidates:
415421
logger.error("Code repair failed to generate a valid candidate.")
416422
return None
@@ -721,6 +727,7 @@ def get_optimization_review(
721727
replay_tests: str,
722728
concolic_tests: str, # noqa: ARG002
723729
calling_fn_details: str,
730+
language: str = "python",
724731
) -> OptimizationReviewResult:
725732
"""Compute the optimization review of current Pull Request.
726733
@@ -762,7 +769,8 @@ def get_optimization_review(
762769
"original_runtime": humanize_runtime(explanation.original_runtime_ns),
763770
"codeflash_version": codeflash_version,
764771
"calling_fn_details": calling_fn_details,
765-
"python_version": platform.python_version(),
772+
"language": language,
773+
"python_version": platform.python_version() if language == "python" else None,
766774
"call_sequence": self.get_next_sequence(),
767775
}
768776
console.rule()

codeflash/code_utils/code_replacer.py

Lines changed: 17 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -498,7 +498,7 @@ def replace_function_definitions_for_language(
498498
from codeflash.languages import get_language_support
499499
from codeflash.languages.base import FunctionInfo, Language, ParentInfo
500500

501-
source_code: str = module_abspath.read_text(encoding="utf8")
501+
original_source_code: str = module_abspath.read_text(encoding="utf8")
502502
code_to_apply = get_optimized_code_for_module(module_abspath.relative_to(project_root_path), optimized_code)
503503

504504
if not code_to_apply.strip():
@@ -522,24 +522,24 @@ def replace_function_definitions_for_language(
522522
is_async=function_to_optimize.is_async,
523523
language=language,
524524
)
525-
new_code = lang_support.replace_function(source_code, func_info, code_to_apply)
525+
new_code = lang_support.replace_function(original_source_code, func_info, code_to_apply)
526526
else:
527527
# Fallback: find function in source and replace
528528
# This is less precise but works when we don't have line info
529529
functions = lang_support.discover_functions(module_abspath)
530+
new_code = original_source_code
530531
for func in functions:
531532
qualified_name = func.qualified_name
532533
if qualified_name in function_names or func.name in function_names:
533-
new_code = lang_support.replace_function(source_code, func, code_to_apply)
534-
source_code = new_code # Continue with modified source for multiple replacements
534+
new_code = lang_support.replace_function(new_code, func, code_to_apply)
535535
break
536536
else:
537537
# No matching function found
538538
logger.warning(f"Could not find function {function_names} in {module_abspath}")
539539
return False
540540

541541
# Check if there was actually a change
542-
if source_code.strip() == new_code.strip():
542+
if original_source_code.strip() == new_code.strip():
543543
return False
544544

545545
module_abspath.write_text(new_code, encoding="utf8")
@@ -550,12 +550,18 @@ def get_optimized_code_for_module(relative_path: Path, optimized_code: CodeStrin
550550
file_to_code_context = optimized_code.file_to_path()
551551
module_optimized_code = file_to_code_context.get(str(relative_path))
552552
if module_optimized_code is None:
553-
logger.warning(
554-
f"Optimized code not found for {relative_path} In the context\n-------\n{optimized_code}\n-------\n"
555-
"re-check your 'markdown code structure'"
556-
f"existing files are {file_to_code_context.keys()}"
557-
)
558-
module_optimized_code = ""
553+
# Fallback for JavaScript/TypeScript: if there's only one code block with None file path,
554+
# use it regardless of the expected path (the AI server doesn't always include file paths)
555+
if "None" in file_to_code_context and len(file_to_code_context) == 1:
556+
module_optimized_code = file_to_code_context["None"]
557+
logger.debug(f"Using code block with None file_path for {relative_path}")
558+
else:
559+
logger.warning(
560+
f"Optimized code not found for {relative_path} In the context\n-------\n{optimized_code}\n-------\n"
561+
"re-check your 'markdown code structure'"
562+
f"existing files are {file_to_code_context.keys()}"
563+
)
564+
module_optimized_code = ""
559565
return module_optimized_code
560566

561567

codeflash/code_utils/deduplicate_code.py

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -155,21 +155,22 @@ def visit_With(self, node): # noqa : ANN001, ANN201
155155

156156

157157
def normalize_code(code: str, remove_docstrings: bool = True, return_ast_dump: bool = False) -> str: # noqa : FBT002, FBT001
158-
"""Normalize Python code by parsing, cleaning, and normalizing only variable names.
158+
"""Normalize code by parsing, cleaning, and normalizing only variable names.
159159
160160
Function names, class names, and parameters are preserved.
161+
For non-Python code (JavaScript, TypeScript), falls back to basic whitespace normalization.
161162
162163
Args:
163-
code: Python source code as string
164-
remove_docstrings: Whether to remove docstrings
165-
return_ast_dump: return_ast_dump
164+
code: Source code as string
165+
remove_docstrings: Whether to remove docstrings (Python only)
166+
return_ast_dump: return_ast_dump (Python only)
166167
167168
Returns:
168169
Normalized code as string
169170
170171
"""
171172
try:
172-
# Parse the code
173+
# Parse the code (Python-specific)
173174
tree = ast.parse(code)
174175

175176
# Remove docstrings if requested
@@ -188,9 +189,10 @@ def normalize_code(code: str, remove_docstrings: bool = True, return_ast_dump: b
188189

189190
# Unparse back to code
190191
return ast.unparse(normalized_tree)
191-
except SyntaxError as e:
192-
msg = f"Invalid Python syntax: {e}"
193-
raise ValueError(msg) from e
192+
except SyntaxError:
193+
# Non-Python code (JavaScript, TypeScript, etc.) - use basic whitespace normalization
194+
# This still allows duplicate detection to work
195+
return " ".join(code.split())
194196

195197

196198
def remove_docstrings_from_ast(node): # noqa : ANN001, ANN201

codeflash/context/unused_definition_remover.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -722,6 +722,11 @@ def detect_unused_helper_functions(
722722
List of FunctionSource objects representing unused helper functions
723723
724724
"""
725+
# Skip this analysis for non-Python languages since we use Python's ast module
726+
if function_to_optimize.language in ("javascript", "typescript"):
727+
logger.debug("Skipping unused helper function detection for JavaScript/TypeScript")
728+
return []
729+
725730
if isinstance(optimized_code, CodeStringsMarkdown) and len(optimized_code.code_strings) > 0:
726731
return list(
727732
chain.from_iterable(

codeflash/languages/javascript/runtime/codeflash-jest-helper.js

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -65,19 +65,20 @@ let db = null;
6565

6666
/**
6767
* Get high-resolution time in nanoseconds.
68-
* Prefers process.hrtime.bigint() for nanosecond precision,
69-
* falls back to performance.now() * 1e6 for non-Node environments.
68+
* Cached at module load time for minimal overhead during timing.
7069
*
71-
* @returns {bigint|number} - Time in nanoseconds
70+
* @returns {bigint} - Time in nanoseconds
7271
*/
73-
function getTimeNs() {
72+
const getTimeNs = (() => {
73+
// Determine timing method once at module load, not on every call
7474
if (typeof process !== 'undefined' && process.hrtime && process.hrtime.bigint) {
75-
return process.hrtime.bigint();
75+
// Node.js with BigInt hrtime support - fastest, most precise
76+
return () => process.hrtime.bigint();
7677
}
77-
// Fallback to performance.now() in milliseconds, converted to nanoseconds
78+
// Fallback: pre-import performance module once
7879
const { performance } = require('perf_hooks');
79-
return BigInt(Math.floor(performance.now() * 1_000_000));
80-
}
80+
return () => BigInt(Math.floor(performance.now() * 1_000_000));
81+
})();
8182

8283
/**
8384
* Calculate duration in nanoseconds.

codeflash/models/models.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,7 @@ class AIServiceCodeRepairRequest:
9393
modified_source_code: str
9494
trace_id: str
9595
test_diffs: list[TestDiff]
96+
language: str = "python"
9697

9798

9899
class OptimizationReviewResult(NamedTuple):
@@ -226,7 +227,9 @@ def validate_code_syntax(self) -> "CodeString":
226227
return self
227228

228229

229-
def get_code_block_splitter(file_path: Path) -> str:
230+
def get_code_block_splitter(file_path: Path | None) -> str:
231+
if file_path is None:
232+
return ""
230233
return f"# file: {file_path.as_posix()}"
231234

232235

codeflash/optimization/function_optimizer.py

Lines changed: 35 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -848,10 +848,14 @@ def handle_successful_candidate(
848848
849849
Returns the BestOptimization and optional benchmark tree.
850850
"""
851-
with progress_bar("Running line-by-line profiling"):
852-
line_profile_test_results = self.line_profiler_step(
853-
code_context=code_context, original_helper_code=original_helper_code, candidate_index=candidate_index
854-
)
851+
# Skip line profiling for JavaScript/TypeScript until implementation is ready
852+
if self.function_to_optimize.language in ("javascript", "typescript"):
853+
line_profile_test_results = {"timings": {}, "unit": 0, "str_out": ""}
854+
else:
855+
with progress_bar("Running line-by-line profiling"):
856+
line_profile_test_results = self.line_profiler_step(
857+
code_context=code_context, original_helper_code=original_helper_code, candidate_index=candidate_index
858+
)
855859

856860
eval_ctx.record_line_profiler_result(candidate.optimization_id, line_profile_test_results["str_out"])
857861

@@ -1314,13 +1318,15 @@ def repair_optimization(
13141318
optimization_id: str,
13151319
ai_service_client: AiServiceClient,
13161320
executor: concurrent.futures.ThreadPoolExecutor,
1321+
language: str = "python",
13171322
) -> concurrent.futures.Future[OptimizedCandidate | None]:
13181323
request = AIServiceCodeRepairRequest(
13191324
optimization_id=optimization_id,
13201325
original_source_code=original_source_code,
13211326
modified_source_code=modified_source_code,
13221327
test_diffs=test_diffs,
13231328
trace_id=trace_id,
1329+
language=language,
13241330
)
13251331
return executor.submit(ai_service_client.code_repair, request=request)
13261332

@@ -2016,6 +2022,7 @@ def process_review(
20162022
"coverage_message": coverage_message,
20172023
"replay_tests": replay_tests,
20182024
"concolic_tests": concolic_tests,
2025+
"language": self.function_to_optimize.language,
20192026
}
20202027

20212028
raise_pr = not self.args.no_pr
@@ -2058,7 +2065,9 @@ def process_review(
20582065
if "root_dir" not in data:
20592066
data["root_dir"] = git_root_dir()
20602067
data["git_remote"] = self.args.git_remote
2061-
check_create_pr(**data)
2068+
# Remove language from data dict as check_create_pr doesn't accept it
2069+
pr_data = {k: v for k, v in data.items() if k != "language"}
2070+
check_create_pr(**pr_data)
20622071
elif staging_review:
20632072
response = create_staging(**data)
20642073
if response.status_code == 200:
@@ -2178,7 +2187,7 @@ def establish_original_code_baseline(
21782187

21792188
# Skip line profiler for JavaScript/TypeScript (not yet supported)
21802189
if self.function_to_optimize.language in ("javascript", "typescript"):
2181-
line_profile_results = None
2190+
line_profile_results = {"timings": {}, "unit": 0, "str_out": ""}
21822191
else:
21832192
with progress_bar("Running line profiler to identify performance bottlenecks..."):
21842193
line_profile_results = self.line_profiler_step(
@@ -2318,6 +2327,7 @@ def repair_if_possible(
23182327
ai_service_client=ai_service_client,
23192328
optimization_id=candidate.optimization_id,
23202329
executor=self.executor,
2330+
language=self.function_to_optimize.language,
23212331
)
23222332
)
23232333

@@ -2386,17 +2396,25 @@ def run_optimized_candidate(
23862396

23872397
# Use language-appropriate comparison
23882398
if self.function_to_optimize.language in ("javascript", "typescript"):
2389-
# JavaScript: Compare using Node.js script (handles Map, Set, Date, etc. natively)
2390-
from codeflash.verification.equivalence import compare_javascript_test_results
2391-
from codeflash.code_utils.code_utils import get_run_tmp_file
2392-
2399+
# JavaScript: Compare using SQLite results if available, otherwise compare test pass/fail
23932400
original_sqlite = get_run_tmp_file(Path("test_return_values_0.sqlite"))
23942401
candidate_sqlite = get_run_tmp_file(Path(f"test_return_values_{optimization_candidate_index}.sqlite"))
2395-
match, diffs = compare_javascript_test_results(original_sqlite, candidate_sqlite)
23962402

2397-
# Cleanup SQLite files after comparison (deferred from parse_test_results)
2398-
candidate_sqlite.unlink(missing_ok=True)
2399-
# Keep original_sqlite for comparing with other candidates
2403+
if original_sqlite.exists() and candidate_sqlite.exists():
2404+
# Full comparison using captured return values
2405+
from codeflash.verification.equivalence import compare_javascript_test_results
2406+
2407+
match, diffs = compare_javascript_test_results(original_sqlite, candidate_sqlite)
2408+
# Cleanup SQLite files after comparison
2409+
candidate_sqlite.unlink(missing_ok=True)
2410+
else:
2411+
# Fallback: compare test pass/fail status (tests aren't instrumented yet)
2412+
# If all tests that passed for original also pass for candidate, consider it a match
2413+
if not candidate_sqlite.exists():
2414+
logger.error(f"Candidate SQLite database not found: {candidate_sqlite}")
2415+
logger.debug("No diffs found, skipping repair")
2416+
# Use Python-style comparison on TestResults as fallback
2417+
match, diffs = compare_test_results(baseline_results.behavior_test_results, candidate_behavior_results)
24002418
else:
24012419
# Python: Compare using Python comparator
24022420
match, diffs = compare_test_results(baseline_results.behavior_test_results, candidate_behavior_results)
@@ -2503,6 +2521,8 @@ def run_and_parse_tests(
25032521
test_env=test_env,
25042522
pytest_timeout=INDIVIDUAL_TESTCASE_TIMEOUT,
25052523
enable_coverage=enable_coverage,
2524+
js_project_root=self.test_cfg.js_project_root,
2525+
candidate_index=optimization_iteration,
25062526
)
25072527
elif testing_type == TestingMode.LINE_PROFILE:
25082528
result_file_path, run_result = run_line_profile_tests(
@@ -2527,6 +2547,7 @@ def run_and_parse_tests(
25272547
pytest_min_loops=pytest_min_loops,
25282548
pytest_max_loops=pytest_max_loops,
25292549
test_framework=self.test_cfg.test_framework,
2550+
js_project_root=self.test_cfg.js_project_root,
25302551
)
25312552
else:
25322553
msg = f"Unexpected testing type: {testing_type}"

0 commit comments

Comments
 (0)