Skip to content

Commit 8ca0f8d

Browse files
committed
Fix JS line profiler empty output file causing JSONDecodeError
The profiler's save() was called every 100 hit() calls. With O(n²) algorithms this produced hundreds of thousands of writeFileSync calls, each truncating the file to 0 bytes before writing. If the subprocess timed out (SIGKILL), the file was left at 0 bytes → JSONDecodeError. Fixes: - Move require('fs')/require('path') to module scope (not inside save()) - Reduce save-every-N from 100 → 10,000 hits (100x fewer syscalls) - Pre-create output file with {} before running Jest (safety net) - Handle empty files gracefully in parse_results - Fix misleading "file not found" warning → "file empty or no timing data"
1 parent 23d9e73 commit 8ca0f8d

3 files changed

Lines changed: 31 additions & 19 deletions

File tree

codeflash/languages/javascript/function_optimizer.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,11 @@ def line_profiler_step(
182182
try:
183183
line_profiler_output_path = get_run_tmp_file(Path("line_profiler_output.json"))
184184

185+
# Pre-create with valid empty JSON so the file is never 0 bytes
186+
# even if the JS profiler save() is interrupted (e.g. SIGKILL on timeout)
187+
line_profiler_output_path.parent.mkdir(parents=True, exist_ok=True)
188+
line_profiler_output_path.write_text("{}", encoding="utf-8")
189+
185190
success = self.language_support.instrument_source_for_line_profiler(
186191
func_info=self.function_to_optimize, line_profiler_output_file=line_profiler_output_path
187192
)

codeflash/languages/javascript/line_profiler.py

Lines changed: 15 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -86,9 +86,10 @@ def _generate_profiler_init(self) -> str:
8686
# Serialize line contents map for embedding in JavaScript
8787
line_contents_json = json.dumps(getattr(self, "line_contents", {}))
8888

89-
return f"""
89+
return f"""// @ts-nocheck
9090
// Codeflash line profiler initialization
91-
// @ts-nocheck
91+
const __codeflash_fs__ = require('fs');
92+
const __codeflash_path__ = require('path');
9293
const {self.profiler_var} = {{
9394
stats: {{}},
9495
lineContents: {line_contents_json},
@@ -123,19 +124,18 @@ def _generate_profiler_init(self) -> str:
123124
this.lastLineTime = now;
124125
125126
this.totalHits++;
126-
// Save every 100 hits to ensure we capture results even with --forceExit
127-
if (this.totalHits % 100 === 0) {{
127+
// Save periodically to capture results even with --forceExit.
128+
// Use 10000 (not 100) to avoid excessive I/O on hot loops.
129+
if (this.totalHits % 10000 === 0) {{
128130
this.save();
129131
}}
130132
}},
131133
132134
save: function() {{
133-
const fs = require('fs');
134-
const pathModule = require('path');
135-
const outputDir = pathModule.dirname('{self.output_file.as_posix()}');
135+
const outputDir = __codeflash_path__.dirname('{self.output_file.as_posix()}');
136136
try {{
137-
if (!fs.existsSync(outputDir)) {{
138-
fs.mkdirSync(outputDir, {{ recursive: true }});
137+
if (!__codeflash_fs__.existsSync(outputDir)) {{
138+
__codeflash_fs__.mkdirSync(outputDir, {{ recursive: true }});
139139
}}
140140
// Merge line contents into stats before saving
141141
const statsWithContent = {{}};
@@ -145,7 +145,7 @@ def _generate_profiler_init(self) -> str:
145145
content: this.lineContents[key] || ''
146146
}};
147147
}}
148-
fs.writeFileSync(
148+
__codeflash_fs__.writeFileSync(
149149
'{self.output_file.as_posix()}',
150150
JSON.stringify(statsWithContent, null, 2)
151151
);
@@ -298,8 +298,11 @@ def parse_results(profile_file: Path) -> dict:
298298
return {"timings": {}, "unit": 1e-9, "functions": {}}
299299

300300
try:
301-
with profile_file.open("r") as f:
302-
data = json.load(f)
301+
content = profile_file.read_text(encoding="utf-8").strip()
302+
if not content:
303+
logger.warning("Line profiler output file is empty: %s", profile_file)
304+
return {"timings": {}, "unit": 1e-9, "functions": {}}
305+
data = json.loads(content)
303306

304307
# Group by file and function
305308
timings = {}

codeflash/languages/javascript/support.py

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2497,13 +2497,17 @@ def instrument_source_for_line_profiler(
24972497
def parse_line_profile_results(self, line_profiler_output_file: Path) -> dict:
24982498
from codeflash.languages.javascript.line_profiler import JavaScriptLineProfiler
24992499

2500-
if line_profiler_output_file.exists():
2501-
parsed_results = JavaScriptLineProfiler.parse_results(line_profiler_output_file)
2502-
if parsed_results.get("timings"):
2503-
# Format output string for display
2504-
str_out = self._format_js_line_profile_output(parsed_results)
2505-
return {"timings": parsed_results.get("timings", {}), "unit": 1e-9, "str_out": str_out}
2506-
logger.warning("No line profiler output file found at %s", line_profiler_output_file)
2500+
if not line_profiler_output_file.exists():
2501+
logger.warning("Line profiler output file not found: %s", line_profiler_output_file)
2502+
return {"timings": {}, "unit": 0, "str_out": ""}
2503+
2504+
parsed_results = JavaScriptLineProfiler.parse_results(line_profiler_output_file)
2505+
if parsed_results.get("timings"):
2506+
# Format output string for display
2507+
str_out = self._format_js_line_profile_output(parsed_results)
2508+
return {"timings": parsed_results.get("timings", {}), "unit": 1e-9, "str_out": str_out}
2509+
2510+
logger.warning("Line profiler output file empty or contained no timing data: %s", line_profiler_output_file)
25072511
return {"timings": {}, "unit": 0, "str_out": ""}
25082512

25092513
def _format_js_line_profile_output(self, parsed_results: dict) -> str:

0 commit comments

Comments
 (0)