## Summary - Reverts lazy JWT_SECRET initialization — keeps eager fail-fast at module load - Adds `JWT_SECRET` secret to both `deploy_cfwebapp_to_azure.yml` and `nextjs-build.yaml` CI workflows so `next build` page data collection succeeds for the `/codeflash/auth/oauth/token` route ## Context The deploy workflow ([run #24425211765](https://github.com/codeflash-ai/codeflash-internal/actions/runs/24425211765/job/71357530269)) was failing because `JWT_SECRET` isn't available during CI build, causing an eager throw at module load time. The secret already exists as a GitHub repo secret.
345 lines
9.8 KiB
TypeScript
345 lines
9.8 KiB
TypeScript
/**
|
|
* Line Profiler Parser Utility
|
|
* Parses the markdown table output from Python's line_profiler into structured data
|
|
*/
|
|
|
|
export interface LineProfilerEntry {
|
|
hits: string
|
|
time: string
|
|
perHit: string
|
|
percentTime: number
|
|
lineContents: string
|
|
}
|
|
|
|
export interface LineProfilerFunction {
|
|
functionName: string
|
|
totalTime: string
|
|
entries: LineProfilerEntry[]
|
|
}
|
|
|
|
export interface LineProfilerReport {
|
|
timerUnit: string
|
|
functions: LineProfilerFunction[]
|
|
}
|
|
|
|
/**
|
|
* Parse line profiler results from the markdown table format
|
|
* Expected format:
|
|
* ```
|
|
* # Timer unit: 1e-09 s
|
|
* ## Function: function_name
|
|
* ## Total time: X s
|
|
* | Hits | Time | Per Hit | % Time | Line Contents |
|
|
* |------|------|---------|--------|---------------|
|
|
* | 123 | 456 | 3.7 | 50.0 | def foo(): |
|
|
* ```
|
|
*
|
|
* TODO: Security & Robustness - Add input validation before processing:
|
|
* - Maximum input size limit (e.g., 10MB) to prevent DoS
|
|
* - Maximum line count limit (e.g., 100,000 lines)
|
|
* - Line length validation
|
|
* - Structure validation (expected headers, format)
|
|
*
|
|
* Example:
|
|
* ```typescript
|
|
* if (rawResults.length > 10_000_000) {
|
|
* throw new Error('Profiler output too large');
|
|
* }
|
|
* const lines = rawResults.split("\n");
|
|
* if (lines.length > 100_000) {
|
|
* throw new Error('Too many lines in profiler output');
|
|
* }
|
|
* ```
|
|
*/
|
|
export function parseLineProfilerResults(rawResults: string): LineProfilerReport {
|
|
const report: LineProfilerReport = {
|
|
timerUnit: "",
|
|
functions: [],
|
|
}
|
|
|
|
if (!rawResults || rawResults.trim() === "") {
|
|
return report
|
|
}
|
|
|
|
// Detect format: JS format starts with "Line Profile Results:" or has space-separated columns
|
|
const isJsFormat =
|
|
rawResults.includes("Line Profile Results:") ||
|
|
(rawResults.includes("Line") &&
|
|
rawResults.includes("Hits") &&
|
|
rawResults.includes("Content") &&
|
|
!rawResults.includes("|"))
|
|
|
|
if (isJsFormat) {
|
|
return parseJsLineProfilerResults(rawResults)
|
|
}
|
|
|
|
return parsePythonLineProfilerResults(rawResults)
|
|
}
|
|
|
|
/**
|
|
* Parse JS/TS line profiler format:
|
|
* Line Profile Results:
|
|
* File: /path/to/file.js
|
|
* --------------------------------------------------------------------------------
|
|
* Line Hits Time (ms) % Time Content
|
|
* --------------------------------------------------------------------------------
|
|
* 12 3442092 397.958 100.0% if (n <= 1) {
|
|
*/
|
|
function parseJsLineProfilerResults(rawResults: string): LineProfilerReport {
|
|
const report: LineProfilerReport = {
|
|
timerUnit: "1e-03 s", // JS profiler uses milliseconds
|
|
functions: [],
|
|
}
|
|
|
|
const lines = rawResults.split("\n")
|
|
let currentFunction: LineProfilerFunction | null = null
|
|
let inData = false
|
|
|
|
for (const line of lines) {
|
|
const trimmedLine = line.trim()
|
|
|
|
// Extract file path as function name
|
|
if (trimmedLine.startsWith("File:")) {
|
|
if (currentFunction) {
|
|
report.functions.push(currentFunction)
|
|
}
|
|
const filePath = trimmedLine.replace("File:", "").trim()
|
|
const fileName = filePath.split("/").pop() || filePath
|
|
currentFunction = {
|
|
functionName: fileName,
|
|
totalTime: "",
|
|
entries: [],
|
|
}
|
|
inData = false
|
|
continue
|
|
}
|
|
|
|
// Skip header line and separator lines
|
|
if (trimmedLine.startsWith("Line") && trimmedLine.includes("Hits")) {
|
|
inData = false
|
|
continue
|
|
}
|
|
if (trimmedLine.match(/^-+$/)) {
|
|
inData = true
|
|
continue
|
|
}
|
|
|
|
// Parse data rows: " 12 3442092 397.958 100.0% if (n <= 1) {"
|
|
if (inData && currentFunction && trimmedLine.length > 0) {
|
|
// Match: line_number, hits, time, percent, content
|
|
const match = trimmedLine.match(/^\s*(\d+)\s+(\d+)\s+([\d.]+)\s+([\d.]+)%\s+(.*)$/)
|
|
if (match) {
|
|
const [, , hits, time, percent, content] = match
|
|
currentFunction.entries.push({
|
|
hits: hits,
|
|
time: time,
|
|
perHit: hits === "0" ? "0.000" : (parseFloat(time) / parseInt(hits)).toFixed(3),
|
|
percentTime: parseFloat(percent) || 0,
|
|
lineContents: content || " ",
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
if (currentFunction) {
|
|
report.functions.push(currentFunction)
|
|
}
|
|
|
|
return report
|
|
}
|
|
|
|
/**
|
|
* Parse Python line_profiler markdown table format
|
|
*/
|
|
function parsePythonLineProfilerResults(rawResults: string): LineProfilerReport {
|
|
const report: LineProfilerReport = {
|
|
timerUnit: "",
|
|
functions: [],
|
|
}
|
|
|
|
const lines = rawResults.split("\n")
|
|
let currentFunction: LineProfilerFunction | null = null
|
|
let inTable = false
|
|
let headerPassed = false
|
|
|
|
for (const line of lines) {
|
|
const trimmedLine = line.trim()
|
|
|
|
// Parse timer unit
|
|
if (trimmedLine.startsWith("# Timer unit:")) {
|
|
report.timerUnit = trimmedLine.replace("# Timer unit:", "").trim()
|
|
continue
|
|
}
|
|
|
|
// Parse function name
|
|
if (trimmedLine.startsWith("## Function:")) {
|
|
if (currentFunction) {
|
|
report.functions.push(currentFunction)
|
|
}
|
|
currentFunction = {
|
|
functionName: trimmedLine.replace("## Function:", "").trim(),
|
|
totalTime: "",
|
|
entries: [],
|
|
}
|
|
inTable = false
|
|
headerPassed = false
|
|
continue
|
|
}
|
|
|
|
// Parse total time
|
|
if (trimmedLine.startsWith("## Total time:")) {
|
|
if (currentFunction) {
|
|
currentFunction.totalTime = trimmedLine.replace("## Total time:", "").trim()
|
|
}
|
|
continue
|
|
}
|
|
|
|
// Detect table header - handle variable whitespace in cells
|
|
// Tabulate may produce "| Hits |" with extra spaces
|
|
if (
|
|
trimmedLine.includes("Hits") &&
|
|
trimmedLine.includes("Line Contents") &&
|
|
trimmedLine.startsWith("|")
|
|
) {
|
|
inTable = true
|
|
headerPassed = false
|
|
continue
|
|
}
|
|
|
|
// Skip separator line (|---|---|...) - also handles alignment colons like |-------:|
|
|
if (inTable && trimmedLine.match(/^\|[-\s|:]+\|$/)) {
|
|
headerPassed = true
|
|
continue
|
|
}
|
|
|
|
// Parse table rows
|
|
if (inTable && headerPassed && trimmedLine.startsWith("|") && currentFunction) {
|
|
// Split by | but preserve the original line for extracting code with whitespace
|
|
const rawParts = trimmedLine.split("|")
|
|
// Trim stats columns but NOT the code column
|
|
const statParts = rawParts.slice(1, 5).map(p => p.trim())
|
|
// Keep code with original whitespace - join remaining parts (code may contain pipes)
|
|
// Use " " for empty lines to preserve blank lines in display
|
|
const codePart = rawParts.slice(5, -1).join("|") || " "
|
|
|
|
if (statParts.length >= 4) {
|
|
const percentStr = statParts[3] || "0"
|
|
const percentTime = parseFloat(percentStr) || 0
|
|
|
|
currentFunction.entries.push({
|
|
hits: statParts[0] || "",
|
|
time: statParts[1] || "",
|
|
perHit: statParts[2] || "",
|
|
percentTime,
|
|
lineContents: codePart, // Preserve whitespace in code
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
// Don't forget the last function
|
|
if (currentFunction) {
|
|
report.functions.push(currentFunction)
|
|
}
|
|
|
|
return report
|
|
}
|
|
|
|
/**
|
|
* Heat level thresholds for performance visualization (% of total time)
|
|
*/
|
|
export const HEAT_THRESHOLDS = {
|
|
HOT_4: 50, // >= 50% - Critical hotspot
|
|
HOT_3: 30, // >= 30% - High impact
|
|
HOT_2: 15, // >= 15% - Medium impact
|
|
HOT_1: 5, // >= 5% - Low impact
|
|
} as const
|
|
|
|
/**
|
|
* Get the heat level for a given percent time
|
|
* Returns a class suffix for CSS styling
|
|
*/
|
|
export function getHeatLevel(percentTime: number): "cold" | "hot-1" | "hot-2" | "hot-3" | "hot-4" {
|
|
if (percentTime >= HEAT_THRESHOLDS.HOT_4) return "hot-4"
|
|
if (percentTime >= HEAT_THRESHOLDS.HOT_3) return "hot-3"
|
|
if (percentTime >= HEAT_THRESHOLDS.HOT_2) return "hot-2"
|
|
if (percentTime >= HEAT_THRESHOLDS.HOT_1) return "hot-1"
|
|
return "cold"
|
|
}
|
|
|
|
/**
|
|
* Calculate the maximum percent time across all entries for normalization
|
|
*/
|
|
export function getMaxPercentTime(report: LineProfilerReport): number {
|
|
let max = 0
|
|
for (const func of report.functions) {
|
|
for (const entry of func.entries) {
|
|
if (entry.percentTime > max) {
|
|
max = entry.percentTime
|
|
}
|
|
}
|
|
}
|
|
return max || 100
|
|
}
|
|
|
|
/**
|
|
* Format timer unit to human-readable string
|
|
* e.g., "1e-09 s" → "1 ns", "1e-06 s" → "1 µs"
|
|
*/
|
|
export function formatTimerUnit(timerUnit: string): string {
|
|
if (!timerUnit || timerUnit.trim() === "") return ""
|
|
|
|
const unitMatch = timerUnit.match(/([0-9.e+-]+)\s*s/)
|
|
if (!unitMatch) return timerUnit
|
|
|
|
const scaleFactor = parseFloat(unitMatch[1])
|
|
|
|
if (scaleFactor >= 1) {
|
|
return `${scaleFactor} s`
|
|
} else if (scaleFactor >= 1e-3) {
|
|
return `${scaleFactor * 1000} ms`
|
|
} else if (scaleFactor >= 1e-6) {
|
|
return `${scaleFactor * 1e6} µs`
|
|
} else if (scaleFactor >= 1e-9) {
|
|
return `${scaleFactor * 1e9} ns`
|
|
} else {
|
|
return `${scaleFactor * 1e12} ps`
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Format time value to human-readable string
|
|
*/
|
|
export function formatTime(timeStr: string, timerUnit: string): string {
|
|
if (!timeStr || timeStr === "") return ""
|
|
|
|
// Parse the timer unit to get the scale factor
|
|
// Common units: "1e-09 s" (nanoseconds), "1e-06 s" (microseconds), etc.
|
|
let scaleFactor = 1e-9 // Default to nanoseconds
|
|
const unitMatch = timerUnit.match(/([0-9.e+-]+)\s*s/)
|
|
if (unitMatch) {
|
|
scaleFactor = parseFloat(unitMatch[1])
|
|
}
|
|
|
|
// Parse the time value
|
|
const timeValue = parseFloat(timeStr.replace(/,/g, ""))
|
|
if (isNaN(timeValue)) return timeStr
|
|
|
|
// Convert to seconds
|
|
const seconds = timeValue * scaleFactor
|
|
|
|
// Format based on magnitude
|
|
if (seconds >= 3600) {
|
|
return `${(seconds / 3600).toFixed(2)}h`
|
|
} else if (seconds >= 60) {
|
|
return `${(seconds / 60).toFixed(2)}m`
|
|
} else if (seconds >= 1) {
|
|
return `${seconds.toFixed(2)}s`
|
|
} else if (seconds >= 0.001) {
|
|
return `${(seconds * 1000).toFixed(2)}ms`
|
|
} else if (seconds >= 0.000001) {
|
|
return `${(seconds * 1000000).toFixed(2)}µs`
|
|
} else {
|
|
return `${(seconds * 1000000000).toFixed(2)}ns`
|
|
}
|
|
}
|