modestbench 0.1.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +31 -0
- package/README.md +162 -57
- package/dist/bootstrap.cjs +10 -10
- package/dist/bootstrap.cjs.map +1 -1
- package/dist/bootstrap.d.cts.map +1 -1
- package/dist/bootstrap.d.ts.map +1 -1
- package/dist/bootstrap.js +5 -5
- package/dist/bootstrap.js.map +1 -1
- package/dist/cli/commands/analyze.cjs +60 -0
- package/dist/cli/commands/analyze.cjs.map +1 -0
- package/dist/cli/commands/analyze.d.cts +35 -0
- package/dist/cli/commands/analyze.d.cts.map +1 -0
- package/dist/cli/commands/analyze.d.ts +35 -0
- package/dist/cli/commands/analyze.d.ts.map +1 -0
- package/dist/cli/commands/analyze.js +56 -0
- package/dist/cli/commands/analyze.js.map +1 -0
- package/dist/cli/commands/baseline.cjs +404 -0
- package/dist/cli/commands/baseline.cjs.map +1 -0
- package/dist/cli/commands/baseline.d.cts +72 -0
- package/dist/cli/commands/baseline.d.cts.map +1 -0
- package/dist/cli/commands/baseline.d.ts +72 -0
- package/dist/cli/commands/baseline.d.ts.map +1 -0
- package/dist/cli/commands/baseline.js +396 -0
- package/dist/cli/commands/baseline.js.map +1 -0
- package/dist/cli/commands/history.cjs +108 -266
- package/dist/cli/commands/history.cjs.map +1 -1
- package/dist/cli/commands/history.d.cts +75 -12
- package/dist/cli/commands/history.d.cts.map +1 -1
- package/dist/cli/commands/history.d.ts +75 -12
- package/dist/cli/commands/history.d.ts.map +1 -1
- package/dist/cli/commands/history.js +105 -268
- package/dist/cli/commands/history.js.map +1 -1
- package/dist/cli/commands/init.cjs +88 -155
- package/dist/cli/commands/init.cjs.map +1 -1
- package/dist/cli/commands/init.d.cts +4 -4
- package/dist/cli/commands/init.d.cts.map +1 -1
- package/dist/cli/commands/init.d.ts +4 -4
- package/dist/cli/commands/init.d.ts.map +1 -1
- package/dist/cli/commands/init.js +88 -155
- package/dist/cli/commands/init.js.map +1 -1
- package/dist/cli/commands/run.cjs +143 -112
- package/dist/cli/commands/run.cjs.map +1 -1
- package/dist/cli/commands/run.d.cts +17 -3
- package/dist/cli/commands/run.d.cts.map +1 -1
- package/dist/cli/commands/run.d.ts +17 -3
- package/dist/cli/commands/run.d.ts.map +1 -1
- package/dist/cli/commands/run.js +142 -78
- package/dist/cli/commands/run.js.map +1 -1
- package/dist/cli/index.cjs +671 -266
- package/dist/cli/index.cjs.map +1 -1
- package/dist/cli/index.d.cts +4 -16
- package/dist/cli/index.d.cts.map +1 -1
- package/dist/cli/index.d.ts +4 -16
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +664 -259
- package/dist/cli/index.js.map +1 -1
- package/dist/config/budget-schema.cjs +172 -0
- package/dist/config/budget-schema.cjs.map +1 -0
- package/dist/config/budget-schema.d.cts +59 -0
- package/dist/config/budget-schema.d.cts.map +1 -0
- package/dist/config/budget-schema.d.ts +59 -0
- package/dist/config/budget-schema.d.ts.map +1 -0
- package/dist/config/budget-schema.js +166 -0
- package/dist/config/budget-schema.js.map +1 -0
- package/dist/config/schema.cjs +182 -2
- package/dist/config/schema.cjs.map +1 -1
- package/dist/config/schema.d.cts +122 -3
- package/dist/config/schema.d.cts.map +1 -1
- package/dist/config/schema.d.ts +122 -3
- package/dist/config/schema.d.ts.map +1 -1
- package/dist/config/schema.js +180 -1
- package/dist/config/schema.js.map +1 -1
- package/dist/constants.cjs +45 -2
- package/dist/constants.cjs.map +1 -1
- package/dist/constants.d.cts +41 -0
- package/dist/constants.d.cts.map +1 -1
- package/dist/constants.d.ts +41 -0
- package/dist/constants.d.ts.map +1 -1
- package/dist/constants.js +44 -1
- package/dist/constants.js.map +1 -1
- package/dist/core/engine.cjs +104 -15
- package/dist/core/engine.cjs.map +1 -1
- package/dist/core/engine.d.cts +7 -4
- package/dist/core/engine.d.cts.map +1 -1
- package/dist/core/engine.d.ts +7 -4
- package/dist/core/engine.d.ts.map +1 -1
- package/dist/core/engine.js +105 -16
- package/dist/core/engine.js.map +1 -1
- package/dist/core/output-path-resolver.cjs +41 -0
- package/dist/core/output-path-resolver.cjs.map +1 -0
- package/dist/core/output-path-resolver.d.cts +10 -0
- package/dist/core/output-path-resolver.d.cts.map +1 -0
- package/dist/core/output-path-resolver.d.ts +10 -0
- package/dist/core/output-path-resolver.d.ts.map +1 -0
- package/dist/core/output-path-resolver.js +37 -0
- package/dist/core/output-path-resolver.js.map +1 -0
- package/dist/errors/base.cjs +12 -3
- package/dist/errors/base.cjs.map +1 -1
- package/dist/errors/base.d.cts +7 -0
- package/dist/errors/base.d.cts.map +1 -1
- package/dist/errors/base.d.ts +7 -0
- package/dist/errors/base.d.ts.map +1 -1
- package/dist/errors/base.js +10 -2
- package/dist/errors/base.js.map +1 -1
- package/dist/errors/budget.cjs +37 -0
- package/dist/errors/budget.cjs.map +1 -0
- package/dist/errors/budget.d.cts +31 -0
- package/dist/errors/budget.d.cts.map +1 -0
- package/dist/errors/budget.d.ts +31 -0
- package/dist/errors/budget.d.ts.map +1 -0
- package/dist/errors/budget.js +33 -0
- package/dist/errors/budget.js.map +1 -0
- package/dist/errors/index.cjs +4 -1
- package/dist/errors/index.cjs.map +1 -1
- package/dist/errors/index.d.cts +1 -0
- package/dist/errors/index.d.cts.map +1 -1
- package/dist/errors/index.d.ts +1 -0
- package/dist/errors/index.d.ts.map +1 -1
- package/dist/errors/index.js +2 -0
- package/dist/errors/index.js.map +1 -1
- package/dist/formatters/history/base.cjs +9 -0
- package/dist/formatters/history/base.cjs.map +1 -0
- package/dist/formatters/history/base.d.cts +26 -0
- package/dist/formatters/history/base.d.cts.map +1 -0
- package/dist/formatters/history/base.d.ts +26 -0
- package/dist/formatters/history/base.d.ts.map +1 -0
- package/dist/formatters/history/base.js +8 -0
- package/dist/formatters/history/base.js.map +1 -0
- package/dist/formatters/history/compare.cjs +127 -0
- package/dist/formatters/history/compare.cjs.map +1 -0
- package/dist/formatters/history/compare.d.cts +21 -0
- package/dist/formatters/history/compare.d.cts.map +1 -0
- package/dist/formatters/history/compare.d.ts +21 -0
- package/dist/formatters/history/compare.d.ts.map +1 -0
- package/dist/formatters/history/compare.js +123 -0
- package/dist/formatters/history/compare.js.map +1 -0
- package/dist/formatters/history/list.cjs +74 -0
- package/dist/formatters/history/list.cjs.map +1 -0
- package/dist/formatters/history/list.d.cts +25 -0
- package/dist/formatters/history/list.d.cts.map +1 -0
- package/dist/formatters/history/list.d.ts +25 -0
- package/dist/formatters/history/list.d.ts.map +1 -0
- package/dist/formatters/history/list.js +70 -0
- package/dist/formatters/history/list.js.map +1 -0
- package/dist/formatters/history/show.cjs +98 -0
- package/dist/formatters/history/show.cjs.map +1 -0
- package/dist/formatters/history/show.d.cts +21 -0
- package/dist/formatters/history/show.d.cts.map +1 -0
- package/dist/formatters/history/show.d.ts +21 -0
- package/dist/formatters/history/show.d.ts.map +1 -0
- package/dist/formatters/history/show.js +94 -0
- package/dist/formatters/history/show.js.map +1 -0
- package/dist/formatters/history/trends.cjs +194 -0
- package/dist/formatters/history/trends.cjs.map +1 -0
- package/dist/formatters/history/trends.d.cts +22 -0
- package/dist/formatters/history/trends.d.cts.map +1 -0
- package/dist/formatters/history/trends.d.ts +22 -0
- package/dist/formatters/history/trends.d.ts.map +1 -0
- package/dist/formatters/history/trends.js +190 -0
- package/dist/formatters/history/trends.js.map +1 -0
- package/dist/formatters/history/visualization.cjs +79 -0
- package/dist/formatters/history/visualization.cjs.map +1 -0
- package/dist/formatters/history/visualization.d.cts +24 -0
- package/dist/formatters/history/visualization.d.cts.map +1 -0
- package/dist/formatters/history/visualization.d.ts +24 -0
- package/dist/formatters/history/visualization.d.ts.map +1 -0
- package/dist/formatters/history/visualization.js +74 -0
- package/dist/formatters/history/visualization.js.map +1 -0
- package/dist/index.cjs +27 -17
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +10 -5
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.ts +10 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +14 -9
- package/dist/index.js.map +1 -1
- package/dist/reporters/csv.cjs +39 -19
- package/dist/reporters/csv.cjs.map +1 -1
- package/dist/reporters/csv.d.cts +4 -7
- package/dist/reporters/csv.d.cts.map +1 -1
- package/dist/reporters/csv.d.ts +4 -7
- package/dist/reporters/csv.d.ts.map +1 -1
- package/dist/reporters/csv.js +38 -18
- package/dist/reporters/csv.js.map +1 -1
- package/dist/reporters/human.cjs +87 -99
- package/dist/reporters/human.cjs.map +1 -1
- package/dist/reporters/human.d.cts +15 -14
- package/dist/reporters/human.d.cts.map +1 -1
- package/dist/reporters/human.d.ts +15 -14
- package/dist/reporters/human.d.ts.map +1 -1
- package/dist/reporters/human.js +68 -80
- package/dist/reporters/human.js.map +1 -1
- package/dist/reporters/json.cjs +25 -50
- package/dist/reporters/json.cjs.map +1 -1
- package/dist/reporters/json.d.cts +3 -29
- package/dist/reporters/json.d.cts.map +1 -1
- package/dist/reporters/json.d.ts +3 -29
- package/dist/reporters/json.d.ts.map +1 -1
- package/dist/reporters/json.js +26 -51
- package/dist/reporters/json.js.map +1 -1
- package/dist/reporters/profile-human.cjs +149 -0
- package/dist/reporters/profile-human.cjs.map +1 -0
- package/dist/reporters/profile-human.d.cts +44 -0
- package/dist/reporters/profile-human.d.cts.map +1 -0
- package/dist/reporters/profile-human.d.ts +44 -0
- package/dist/reporters/profile-human.d.ts.map +1 -0
- package/dist/reporters/profile-human.js +142 -0
- package/dist/reporters/profile-human.js.map +1 -0
- package/dist/reporters/simple.cjs +66 -46
- package/dist/reporters/simple.cjs.map +1 -1
- package/dist/reporters/simple.d.cts +15 -15
- package/dist/reporters/simple.d.cts.map +1 -1
- package/dist/reporters/simple.d.ts +15 -15
- package/dist/reporters/simple.d.ts.map +1 -1
- package/dist/reporters/simple.js +65 -45
- package/dist/reporters/simple.js.map +1 -1
- package/dist/schema/modestbench-config.schema.json +153 -0
- package/dist/services/baseline-storage.cjs +151 -0
- package/dist/services/baseline-storage.cjs.map +1 -0
- package/dist/services/baseline-storage.d.cts +55 -0
- package/dist/services/baseline-storage.d.cts.map +1 -0
- package/dist/services/baseline-storage.d.ts +55 -0
- package/dist/services/baseline-storage.d.ts.map +1 -0
- package/dist/services/baseline-storage.js +147 -0
- package/dist/services/baseline-storage.js.map +1 -0
- package/dist/services/budget-evaluator.cjs +146 -0
- package/dist/services/budget-evaluator.cjs.map +1 -0
- package/dist/services/budget-evaluator.d.cts +29 -0
- package/dist/services/budget-evaluator.d.cts.map +1 -0
- package/dist/services/budget-evaluator.d.ts +29 -0
- package/dist/services/budget-evaluator.d.ts.map +1 -0
- package/dist/services/budget-evaluator.js +142 -0
- package/dist/services/budget-evaluator.js.map +1 -0
- package/dist/{config/manager.cjs → services/config-manager.cjs} +25 -11
- package/dist/services/config-manager.cjs.map +1 -0
- package/dist/{config/manager.d.cts → services/config-manager.d.cts} +7 -2
- package/dist/services/config-manager.d.cts.map +1 -0
- package/dist/{config/manager.d.ts → services/config-manager.d.ts} +7 -2
- package/dist/services/config-manager.d.ts.map +1 -0
- package/dist/{config/manager.js → services/config-manager.js} +25 -11
- package/dist/services/config-manager.js.map +1 -0
- package/dist/{core/loader.cjs → services/file-loader.cjs} +5 -8
- package/dist/services/file-loader.cjs.map +1 -0
- package/dist/{core/loader.d.cts → services/file-loader.d.cts} +1 -1
- package/dist/services/file-loader.d.cts.map +1 -0
- package/dist/{core/loader.d.ts → services/file-loader.d.ts} +1 -1
- package/dist/services/file-loader.d.ts.map +1 -0
- package/dist/{core/loader.js → services/file-loader.js} +5 -8
- package/dist/services/file-loader.js.map +1 -0
- package/dist/services/history/comparison.cjs +124 -0
- package/dist/services/history/comparison.cjs.map +1 -0
- package/dist/services/history/comparison.d.cts +18 -0
- package/dist/services/history/comparison.d.cts.map +1 -0
- package/dist/services/history/comparison.d.ts +18 -0
- package/dist/services/history/comparison.d.ts.map +1 -0
- package/dist/services/history/comparison.js +120 -0
- package/dist/services/history/comparison.js.map +1 -0
- package/dist/services/history/models.cjs +9 -0
- package/dist/services/history/models.cjs.map +1 -0
- package/dist/services/history/models.d.cts +139 -0
- package/dist/services/history/models.d.cts.map +1 -0
- package/dist/services/history/models.d.ts +139 -0
- package/dist/services/history/models.d.ts.map +1 -0
- package/dist/services/history/models.js +8 -0
- package/dist/services/history/models.js.map +1 -0
- package/dist/services/history/query.cjs +97 -0
- package/dist/services/history/query.cjs.map +1 -0
- package/dist/services/history/query.d.cts +38 -0
- package/dist/services/history/query.d.cts.map +1 -0
- package/dist/services/history/query.d.ts +38 -0
- package/dist/services/history/query.d.ts.map +1 -0
- package/dist/services/history/query.js +92 -0
- package/dist/services/history/query.js.map +1 -0
- package/dist/services/history/trend-analysis.cjs +187 -0
- package/dist/services/history/trend-analysis.cjs.map +1 -0
- package/dist/services/history/trend-analysis.d.cts +34 -0
- package/dist/services/history/trend-analysis.d.cts.map +1 -0
- package/dist/services/history/trend-analysis.d.ts +34 -0
- package/dist/services/history/trend-analysis.d.ts.map +1 -0
- package/dist/services/history/trend-analysis.js +179 -0
- package/dist/services/history/trend-analysis.js.map +1 -0
- package/dist/{storage/history.cjs → services/history-storage.cjs} +1 -1
- package/dist/services/history-storage.cjs.map +1 -0
- package/dist/{storage/history.d.cts → services/history-storage.d.cts} +1 -1
- package/dist/services/history-storage.d.cts.map +1 -0
- package/dist/{storage/history.d.ts → services/history-storage.d.ts} +1 -1
- package/dist/services/history-storage.d.ts.map +1 -0
- package/dist/{storage/history.js → services/history-storage.js} +1 -1
- package/dist/services/history-storage.js.map +1 -0
- package/dist/services/profiler/profile-filter.cjs +113 -0
- package/dist/services/profiler/profile-filter.cjs.map +1 -0
- package/dist/services/profiler/profile-filter.d.cts +20 -0
- package/dist/services/profiler/profile-filter.d.cts.map +1 -0
- package/dist/services/profiler/profile-filter.d.ts +20 -0
- package/dist/services/profiler/profile-filter.d.ts.map +1 -0
- package/dist/services/profiler/profile-filter.js +109 -0
- package/dist/services/profiler/profile-filter.js.map +1 -0
- package/dist/services/profiler/profile-parser.cjs +139 -0
- package/dist/services/profiler/profile-parser.cjs.map +1 -0
- package/dist/services/profiler/profile-parser.d.cts +18 -0
- package/dist/services/profiler/profile-parser.d.cts.map +1 -0
- package/dist/services/profiler/profile-parser.d.ts +18 -0
- package/dist/services/profiler/profile-parser.d.ts.map +1 -0
- package/dist/services/profiler/profile-parser.js +132 -0
- package/dist/services/profiler/profile-parser.js.map +1 -0
- package/dist/services/profiler/profile-runner.cjs +90 -0
- package/dist/services/profiler/profile-runner.cjs.map +1 -0
- package/dist/services/profiler/profile-runner.d.cts +29 -0
- package/dist/services/profiler/profile-runner.d.cts.map +1 -0
- package/dist/services/profiler/profile-runner.d.ts +29 -0
- package/dist/services/profiler/profile-runner.d.ts.map +1 -0
- package/dist/services/profiler/profile-runner.js +86 -0
- package/dist/services/profiler/profile-runner.js.map +1 -0
- package/dist/{progress/manager.cjs → services/progress-manager.cjs} +1 -1
- package/dist/services/progress-manager.cjs.map +1 -0
- package/dist/{progress/manager.d.cts → services/progress-manager.d.cts} +1 -1
- package/dist/services/progress-manager.d.cts.map +1 -0
- package/dist/{progress/manager.d.ts → services/progress-manager.d.ts} +1 -1
- package/dist/services/progress-manager.d.ts.map +1 -0
- package/dist/{progress/manager.js → services/progress-manager.js} +1 -1
- package/dist/services/progress-manager.js.map +1 -0
- package/dist/{reporters/registry.cjs → services/reporter-registry.cjs} +19 -25
- package/dist/services/reporter-registry.cjs.map +1 -0
- package/dist/{reporters/registry.d.cts → services/reporter-registry.d.cts} +19 -41
- package/dist/services/reporter-registry.d.cts.map +1 -0
- package/dist/{reporters/registry.d.ts → services/reporter-registry.d.ts} +19 -41
- package/dist/services/reporter-registry.d.ts.map +1 -0
- package/dist/{reporters/registry.js → services/reporter-registry.js} +19 -25
- package/dist/services/reporter-registry.js.map +1 -0
- package/dist/types/budgets.cjs +8 -0
- package/dist/types/budgets.cjs.map +1 -0
- package/dist/types/budgets.d.cts +149 -0
- package/dist/types/budgets.d.cts.map +1 -0
- package/dist/types/budgets.d.ts +149 -0
- package/dist/types/budgets.d.ts.map +1 -0
- package/dist/types/budgets.js +7 -0
- package/dist/types/budgets.js.map +1 -0
- package/dist/types/cli.cjs +2 -11
- package/dist/types/cli.cjs.map +1 -1
- package/dist/types/cli.d.cts +3 -224
- package/dist/types/cli.d.cts.map +1 -1
- package/dist/types/cli.d.ts +3 -224
- package/dist/types/cli.d.ts.map +1 -1
- package/dist/types/cli.js +2 -11
- package/dist/types/cli.js.map +1 -1
- package/dist/types/core.cjs +6 -1
- package/dist/types/core.cjs.map +1 -1
- package/dist/types/core.d.cts +13 -2
- package/dist/types/core.d.cts.map +1 -1
- package/dist/types/core.d.ts +13 -2
- package/dist/types/core.d.ts.map +1 -1
- package/dist/types/core.js +2 -1
- package/dist/types/core.js.map +1 -1
- package/dist/types/index.cjs +5 -0
- package/dist/types/index.cjs.map +1 -1
- package/dist/types/index.d.cts +2 -0
- package/dist/types/index.d.cts.map +1 -1
- package/dist/types/index.d.ts +2 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/index.js +2 -0
- package/dist/types/index.js.map +1 -1
- package/dist/types/interfaces.d.cts +15 -8
- package/dist/types/interfaces.d.cts.map +1 -1
- package/dist/types/interfaces.d.ts +15 -8
- package/dist/types/interfaces.d.ts.map +1 -1
- package/dist/types/profiler.cjs +11 -0
- package/dist/types/profiler.cjs.map +1 -0
- package/dist/types/profiler.d.cts +100 -0
- package/dist/types/profiler.d.cts.map +1 -0
- package/dist/types/profiler.d.ts +100 -0
- package/dist/types/profiler.d.ts.map +1 -0
- package/dist/types/profiler.js +10 -0
- package/dist/types/profiler.js.map +1 -0
- package/dist/types/utility.cjs.map +1 -1
- package/dist/types/utility.d.cts +0 -8
- package/dist/types/utility.d.cts.map +1 -1
- package/dist/types/utility.d.ts +0 -8
- package/dist/types/utility.d.ts.map +1 -1
- package/dist/types/utility.js.map +1 -1
- package/dist/utils/ansi.cjs +61 -0
- package/dist/utils/ansi.cjs.map +1 -0
- package/dist/utils/ansi.d.cts +53 -0
- package/dist/utils/ansi.d.cts.map +1 -0
- package/dist/utils/ansi.d.ts +53 -0
- package/dist/utils/ansi.d.ts.map +1 -0
- package/dist/utils/ansi.js +57 -0
- package/dist/utils/ansi.js.map +1 -0
- package/dist/utils/identifiers.cjs +32 -0
- package/dist/utils/identifiers.cjs.map +1 -0
- package/dist/utils/identifiers.d.cts +32 -0
- package/dist/utils/identifiers.d.cts.map +1 -0
- package/dist/utils/identifiers.d.ts +32 -0
- package/dist/utils/identifiers.d.ts.map +1 -0
- package/dist/utils/identifiers.js +27 -0
- package/dist/utils/identifiers.js.map +1 -0
- package/dist/utils/package.cjs +40 -0
- package/dist/utils/package.cjs.map +1 -0
- package/dist/utils/package.d.cts +15 -0
- package/dist/utils/package.d.cts.map +1 -0
- package/dist/utils/package.d.ts +15 -0
- package/dist/utils/package.d.ts.map +1 -0
- package/dist/utils/package.js +33 -0
- package/dist/utils/package.js.map +1 -0
- package/dist/utils/type-guards.cjs +48 -0
- package/dist/utils/type-guards.cjs.map +1 -0
- package/dist/utils/type-guards.d.cts +22 -0
- package/dist/utils/type-guards.d.cts.map +1 -0
- package/dist/utils/type-guards.d.ts +22 -0
- package/dist/utils/type-guards.d.ts.map +1 -0
- package/dist/utils/type-guards.js +43 -0
- package/dist/utils/type-guards.js.map +1 -0
- package/package.json +14 -13
- package/src/bootstrap.ts +5 -5
- package/src/cli/commands/analyze.ts +101 -0
- package/src/cli/commands/baseline.ts +577 -0
- package/src/cli/commands/history.ts +194 -342
- package/src/cli/commands/init.ts +105 -183
- package/src/cli/commands/run.ts +186 -88
- package/src/cli/index.ts +731 -234
- package/src/config/budget-schema.ts +189 -0
- package/src/config/schema.ts +260 -1
- package/src/constants.ts +53 -1
- package/src/core/engine.ts +153 -14
- package/src/core/output-path-resolver.ts +46 -0
- package/src/errors/base.ts +11 -2
- package/src/errors/budget.ts +38 -0
- package/src/errors/index.ts +3 -0
- package/src/formatters/history/base.ts +28 -0
- package/src/formatters/history/compare.ts +186 -0
- package/src/formatters/history/list.ts +101 -0
- package/src/formatters/history/show.ts +155 -0
- package/src/formatters/history/trends.ts +281 -0
- package/src/formatters/history/visualization.ts +93 -0
- package/src/index.ts +17 -12
- package/src/reporters/csv.ts +55 -26
- package/src/reporters/human.ts +90 -89
- package/src/reporters/json.ts +27 -72
- package/src/reporters/profile-human.ts +204 -0
- package/src/reporters/simple.ts +85 -54
- package/src/services/baseline-storage.ts +199 -0
- package/src/services/budget-evaluator.ts +182 -0
- package/src/{config/manager.ts → services/config-manager.ts} +24 -9
- package/src/{core/loader.ts → services/file-loader.ts} +4 -7
- package/src/services/history/comparison.ts +130 -0
- package/src/services/history/models.ts +148 -0
- package/src/services/history/query.ts +116 -0
- package/src/services/history/trend-analysis.ts +238 -0
- package/src/services/profiler/profile-filter.ts +143 -0
- package/src/services/profiler/profile-parser.ts +194 -0
- package/src/services/profiler/profile-runner.ts +121 -0
- package/src/{reporters/registry.ts → services/reporter-registry.ts} +46 -81
- package/src/types/budgets.ts +180 -0
- package/src/types/cli.ts +5 -235
- package/src/types/core.ts +50 -10
- package/src/types/index.ts +5 -0
- package/src/types/interfaces.ts +16 -6
- package/src/types/profiler.ts +132 -0
- package/src/types/utility.ts +0 -10
- package/src/utils/ansi.ts +59 -0
- package/src/utils/identifiers.ts +58 -0
- package/src/utils/package.ts +35 -0
- package/src/utils/type-guards.ts +51 -0
- package/dist/config/manager.cjs.map +0 -1
- package/dist/config/manager.d.cts.map +0 -1
- package/dist/config/manager.d.ts.map +0 -1
- package/dist/config/manager.js.map +0 -1
- package/dist/core/loader.cjs.map +0 -1
- package/dist/core/loader.d.cts.map +0 -1
- package/dist/core/loader.d.ts.map +0 -1
- package/dist/core/loader.js.map +0 -1
- package/dist/progress/manager.cjs.map +0 -1
- package/dist/progress/manager.d.cts.map +0 -1
- package/dist/progress/manager.d.ts.map +0 -1
- package/dist/progress/manager.js.map +0 -1
- package/dist/reporters/registry.cjs.map +0 -1
- package/dist/reporters/registry.d.cts.map +0 -1
- package/dist/reporters/registry.d.ts.map +0 -1
- package/dist/reporters/registry.js.map +0 -1
- package/dist/storage/history.cjs.map +0 -1
- package/dist/storage/history.d.cts.map +0 -1
- package/dist/storage/history.d.ts.map +0 -1
- package/dist/storage/history.js.map +0 -1
- /package/src/{storage/history.ts → services/history-storage.ts} +0 -0
- /package/src/{progress/manager.ts → services/progress-manager.ts} +0 -0
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Trend Analysis Service
|
|
3
|
+
*
|
|
4
|
+
* Statistical analysis of benchmark performance trends over time, including
|
|
5
|
+
* regression detection and trend classification.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { BenchmarkRun } from '../../types/index.js';
|
|
9
|
+
import type {
|
|
10
|
+
TrendData,
|
|
11
|
+
TrendDataPoint,
|
|
12
|
+
TrendsResult,
|
|
13
|
+
TrendStatistics,
|
|
14
|
+
} from './models.js';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Service for analyzing performance trends
|
|
18
|
+
*/
|
|
19
|
+
export class TrendAnalysisService {
|
|
20
|
+
/**
|
|
21
|
+
* Analyze trends across multiple benchmark runs
|
|
22
|
+
*/
|
|
23
|
+
analyzeTrends(runs: BenchmarkRun[]): TrendsResult {
|
|
24
|
+
// Build trend data for each task across runs
|
|
25
|
+
const taskTrendsMap = new Map<string, TrendDataPoint[]>();
|
|
26
|
+
|
|
27
|
+
for (const run of runs) {
|
|
28
|
+
for (const file of run.files) {
|
|
29
|
+
for (const suite of file.suites) {
|
|
30
|
+
for (const task of suite.tasks) {
|
|
31
|
+
if (!task.error) {
|
|
32
|
+
const key = `${file.filePath}::${suite.name}::${task.name}`;
|
|
33
|
+
const dataPoints = taskTrendsMap.get(key) || [];
|
|
34
|
+
dataPoints.push({
|
|
35
|
+
date: new Date(run.startTime),
|
|
36
|
+
mean: task.mean,
|
|
37
|
+
});
|
|
38
|
+
taskTrendsMap.set(key, dataPoints);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Calculate trends for each task
|
|
46
|
+
const trendsData: TrendData[] = [];
|
|
47
|
+
|
|
48
|
+
for (const [key, dataPoints] of taskTrendsMap.entries()) {
|
|
49
|
+
// Sort by date (oldest first)
|
|
50
|
+
dataPoints.sort((a, b) => a.date.getTime() - b.date.getTime());
|
|
51
|
+
|
|
52
|
+
const [_filePath, suiteName, taskName] = key.split('::');
|
|
53
|
+
|
|
54
|
+
const statistics = calculateStatistics(dataPoints);
|
|
55
|
+
const trend = calculateTrend(dataPoints);
|
|
56
|
+
const percentChange = calculatePercentChange(dataPoints);
|
|
57
|
+
|
|
58
|
+
trendsData.push({
|
|
59
|
+
confidence: 95, // Fixed confidence level for now
|
|
60
|
+
dataPoints,
|
|
61
|
+
percentChange,
|
|
62
|
+
runs: dataPoints.length,
|
|
63
|
+
statistics,
|
|
64
|
+
task: `${suiteName} › ${taskName}`,
|
|
65
|
+
trend,
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Sort by absolute percent change (most significant first)
|
|
70
|
+
trendsData.sort(
|
|
71
|
+
(a, b) => Math.abs(b.percentChange) - Math.abs(a.percentChange),
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
// Calculate summary statistics
|
|
75
|
+
const summary = {
|
|
76
|
+
degradingTasks: trendsData.filter((t) => t.trend === 'degrading').length,
|
|
77
|
+
improvingTasks: trendsData.filter((t) => t.trend === 'improving').length,
|
|
78
|
+
stableTasks: trendsData.filter((t) => t.trend === 'stable').length,
|
|
79
|
+
totalTasks: trendsData.length,
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
// Get timespan
|
|
83
|
+
const firstRun = runs[runs.length - 1];
|
|
84
|
+
const lastRun = runs[0];
|
|
85
|
+
const timespan = {
|
|
86
|
+
end: lastRun ? new Date(lastRun.startTime) : new Date(),
|
|
87
|
+
start: firstRun ? new Date(firstRun.startTime) : new Date(),
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
// Detect regressions (require minimum 5 runs for confidence)
|
|
91
|
+
const minRunsForRegression = 5;
|
|
92
|
+
const regressions = trendsData.filter((t) =>
|
|
93
|
+
detectRegression(t, 5, minRunsForRegression),
|
|
94
|
+
);
|
|
95
|
+
|
|
96
|
+
// Also track low-confidence potential regressions (2-4 runs showing degradation)
|
|
97
|
+
const lowConfidenceRegressions = trendsData.filter(
|
|
98
|
+
(t) =>
|
|
99
|
+
t.trend === 'degrading' &&
|
|
100
|
+
t.percentChange >= 5 &&
|
|
101
|
+
t.runs < minRunsForRegression &&
|
|
102
|
+
t.runs >= 2,
|
|
103
|
+
);
|
|
104
|
+
|
|
105
|
+
return {
|
|
106
|
+
lowConfidenceRegressions,
|
|
107
|
+
regressions,
|
|
108
|
+
runs: runs.length,
|
|
109
|
+
summary,
|
|
110
|
+
timespan,
|
|
111
|
+
trends: trendsData,
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Calculate percent change from first to last data point
|
|
118
|
+
*/
|
|
119
|
+
export const calculatePercentChange = (
|
|
120
|
+
dataPoints: TrendDataPoint[],
|
|
121
|
+
): number => {
|
|
122
|
+
if (dataPoints.length === 0 || dataPoints.length === 1) {
|
|
123
|
+
return 0;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const firstPoint = dataPoints[0];
|
|
127
|
+
const lastPoint = dataPoints[dataPoints.length - 1];
|
|
128
|
+
|
|
129
|
+
if (!firstPoint || !lastPoint) {
|
|
130
|
+
return 0;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const first = firstPoint.mean;
|
|
134
|
+
const last = lastPoint.mean;
|
|
135
|
+
|
|
136
|
+
if (first === 0) {
|
|
137
|
+
return 0; // Avoid division by zero
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return ((last - first) / first) * 100;
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Calculate statistical metrics from data points
|
|
145
|
+
*/
|
|
146
|
+
export const calculateStatistics = (
|
|
147
|
+
dataPoints: TrendDataPoint[],
|
|
148
|
+
): TrendStatistics => {
|
|
149
|
+
if (dataPoints.length === 0) {
|
|
150
|
+
throw new Error('Cannot calculate statistics for empty data points array');
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const values = dataPoints.map((dp) => dp.mean);
|
|
154
|
+
const n = values.length;
|
|
155
|
+
|
|
156
|
+
// Calculate mean
|
|
157
|
+
const mean = values.reduce((sum, val) => sum + val, 0) / n;
|
|
158
|
+
|
|
159
|
+
// Calculate median
|
|
160
|
+
const sorted = [...values].sort((a, b) => a - b);
|
|
161
|
+
const median =
|
|
162
|
+
n % 2 === 0
|
|
163
|
+
? ((sorted[n / 2 - 1] ?? 0) + (sorted[n / 2] ?? 0)) / 2
|
|
164
|
+
: (sorted[Math.floor(n / 2)] ?? 0);
|
|
165
|
+
|
|
166
|
+
// Calculate variance and standard deviation
|
|
167
|
+
const variance =
|
|
168
|
+
n === 1
|
|
169
|
+
? 0
|
|
170
|
+
: values.reduce((sum, val) => sum + Math.pow(val - mean, 2), 0) / n;
|
|
171
|
+
const stdDeviation = Math.sqrt(variance);
|
|
172
|
+
|
|
173
|
+
return {
|
|
174
|
+
mean,
|
|
175
|
+
median,
|
|
176
|
+
stdDeviation,
|
|
177
|
+
variance,
|
|
178
|
+
};
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Calculate trend direction from data points using linear regression
|
|
183
|
+
*/
|
|
184
|
+
export const calculateTrend = (
|
|
185
|
+
dataPoints: TrendDataPoint[],
|
|
186
|
+
): 'degrading' | 'improving' | 'stable' => {
|
|
187
|
+
if (dataPoints.length === 0) {
|
|
188
|
+
return 'stable';
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
if (dataPoints.length === 1) {
|
|
192
|
+
return 'stable';
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Simple linear regression to determine slope
|
|
196
|
+
const n = dataPoints.length;
|
|
197
|
+
const x = Array.from({ length: n }, (_, i) => i); // Time indices
|
|
198
|
+
const y = dataPoints.map((dp) => dp.mean);
|
|
199
|
+
|
|
200
|
+
const sumX = x.reduce((a, b) => a + b, 0);
|
|
201
|
+
const sumY = y.reduce((a, b) => a + b, 0);
|
|
202
|
+
const sumXY = x.reduce((sum, xi, i) => sum + xi * (y[i] ?? 0), 0);
|
|
203
|
+
const sumXX = x.reduce((sum, xi) => sum + xi * xi, 0);
|
|
204
|
+
|
|
205
|
+
// Calculate slope: (n*sumXY - sumX*sumY) / (n*sumXX - sumX*sumX)
|
|
206
|
+
const slope = (n * sumXY - sumX * sumY) / (n * sumXX - sumX * sumX);
|
|
207
|
+
|
|
208
|
+
// Determine significance threshold (5% of mean)
|
|
209
|
+
const meanValue = sumY / n;
|
|
210
|
+
const significanceThreshold = Math.abs(meanValue * 0.05);
|
|
211
|
+
|
|
212
|
+
// Classify trend based on slope
|
|
213
|
+
if (Math.abs(slope) < significanceThreshold / n) {
|
|
214
|
+
return 'stable';
|
|
215
|
+
} else if (slope < 0) {
|
|
216
|
+
// Negative slope = values decreasing = performance improving
|
|
217
|
+
return 'improving';
|
|
218
|
+
} else {
|
|
219
|
+
// Positive slope = values increasing = performance degrading
|
|
220
|
+
return 'degrading';
|
|
221
|
+
}
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Detect if a trend represents a performance regression
|
|
226
|
+
*/
|
|
227
|
+
export const detectRegression = (
|
|
228
|
+
trendData: TrendData,
|
|
229
|
+
threshold: number,
|
|
230
|
+
minRuns: number,
|
|
231
|
+
): boolean => {
|
|
232
|
+
// Regression is a degrading trend with percent change exceeding threshold
|
|
233
|
+
return (
|
|
234
|
+
trendData.trend === 'degrading' &&
|
|
235
|
+
trendData.percentChange >= threshold &&
|
|
236
|
+
trendData.runs >= minRuns
|
|
237
|
+
);
|
|
238
|
+
};
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Profile Filter Service
|
|
3
|
+
*
|
|
4
|
+
* Filters and sorts profiled functions based on configuration. Implements smart
|
|
5
|
+
* detection to focus on user code by excluding node_modules and Node.js
|
|
6
|
+
* internals.
|
|
7
|
+
*
|
|
8
|
+
* @packageDocumentation
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { minimatch } from 'minimatch';
|
|
12
|
+
|
|
13
|
+
import type {
|
|
14
|
+
FilteredProfileData,
|
|
15
|
+
ProfileConfig,
|
|
16
|
+
ProfiledFunction,
|
|
17
|
+
RawProfileData,
|
|
18
|
+
} from '../../types/profiler.js';
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Filter profile data based on configuration
|
|
22
|
+
*
|
|
23
|
+
* @param data - Raw profile data
|
|
24
|
+
* @param config - Filter configuration
|
|
25
|
+
* @param packageRoot - Package root directory for smart detection
|
|
26
|
+
* @returns Filtered profile data
|
|
27
|
+
*/
|
|
28
|
+
export const filterProfile = (
|
|
29
|
+
data: RawProfileData,
|
|
30
|
+
config: ProfileConfig,
|
|
31
|
+
packageRoot: string,
|
|
32
|
+
): FilteredProfileData => {
|
|
33
|
+
let filtered = data.functions.filter((fn) => {
|
|
34
|
+
// Only JavaScript functions
|
|
35
|
+
if (fn.category !== 'JavaScript') {
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Apply smart detection if enabled
|
|
40
|
+
if (config.smartDetection && !config.focus?.length) {
|
|
41
|
+
if (!isUserCode(fn.file, packageRoot)) {
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Apply focus patterns (if provided, overrides smart detection)
|
|
47
|
+
if (config.focus?.length) {
|
|
48
|
+
if (!matchesAnyPattern(fn.file, config.focus)) {
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Apply exclude patterns (always applied if provided)
|
|
54
|
+
if (config.exclude?.length) {
|
|
55
|
+
if (matchesAnyPattern(fn.file, config.exclude)) {
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return true;
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
// Apply percentage threshold
|
|
64
|
+
const minPercent = config.minExecutionPercent ?? 0.5;
|
|
65
|
+
filtered = filtered.filter((fn) => fn.percentage >= minPercent);
|
|
66
|
+
|
|
67
|
+
// Sort by percentage (highest first)
|
|
68
|
+
filtered.sort((a, b) => b.percentage - a.percentage);
|
|
69
|
+
|
|
70
|
+
// Limit to topN
|
|
71
|
+
const topN = config.topN ?? 25;
|
|
72
|
+
filtered = filtered.slice(0, topN);
|
|
73
|
+
|
|
74
|
+
// Group by file if requested
|
|
75
|
+
let groupedByFile: Map<string, ProfiledFunction[]> | undefined;
|
|
76
|
+
if (config.groupByFile) {
|
|
77
|
+
groupedByFile = groupByFile(filtered);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return {
|
|
81
|
+
functions: filtered,
|
|
82
|
+
groupedByFile,
|
|
83
|
+
summary: data.summary,
|
|
84
|
+
totalFiltered: data.functions.length,
|
|
85
|
+
totalShown: filtered.length,
|
|
86
|
+
totalTicks: data.totalTicks,
|
|
87
|
+
};
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Check if a file path is user code (not node_modules or internals)
|
|
92
|
+
*/
|
|
93
|
+
const isUserCode = (filePath: string, packageRoot: string): boolean => {
|
|
94
|
+
// Exclude node_modules
|
|
95
|
+
if (
|
|
96
|
+
filePath.includes('/node_modules/') ||
|
|
97
|
+
filePath.includes('\\node_modules\\')
|
|
98
|
+
) {
|
|
99
|
+
return false;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Exclude Node.js internals
|
|
103
|
+
if (filePath.startsWith('node:') || filePath.startsWith('internal/')) {
|
|
104
|
+
return false;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Allow <unknown> files (could be eval'd code or other user code without file paths)
|
|
108
|
+
if (filePath === '<unknown>' || filePath === '[eval]') {
|
|
109
|
+
return true;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Must be within package root
|
|
113
|
+
return filePath.startsWith(packageRoot);
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Check if a file path matches any of the given glob patterns
|
|
118
|
+
*/
|
|
119
|
+
const matchesAnyPattern = (filePath: string, patterns: string[]): boolean => {
|
|
120
|
+
return patterns.some((pattern) => minimatch(filePath, pattern));
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Group functions by file path
|
|
125
|
+
*/
|
|
126
|
+
const groupByFile = (
|
|
127
|
+
functions: ProfiledFunction[],
|
|
128
|
+
): Map<string, ProfiledFunction[]> => {
|
|
129
|
+
const grouped = new Map<string, ProfiledFunction[]>();
|
|
130
|
+
|
|
131
|
+
for (const fn of functions) {
|
|
132
|
+
const existing = grouped.get(fn.file) || [];
|
|
133
|
+
existing.push(fn);
|
|
134
|
+
grouped.set(fn.file, existing);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Sort functions within each file by percentage
|
|
138
|
+
for (const [, fns] of grouped.entries()) {
|
|
139
|
+
fns.sort((a, b) => b.percentage - a.percentage);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return grouped;
|
|
143
|
+
};
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Profile Parser Service
|
|
3
|
+
*
|
|
4
|
+
* Parses Chrome DevTools CPU profile format (*.cpuprofile files) generated by
|
|
5
|
+
* Node.js --cpu-prof flag. Extracts JavaScript function execution data
|
|
6
|
+
* including file paths, line numbers, hit counts, and percentages.
|
|
7
|
+
*
|
|
8
|
+
* @packageDocumentation
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { readFile } from 'node:fs/promises';
|
|
12
|
+
import path from 'node:path';
|
|
13
|
+
import { fileURLToPath } from 'node:url';
|
|
14
|
+
|
|
15
|
+
import type {
|
|
16
|
+
ProfiledFunction,
|
|
17
|
+
ProfileSummary,
|
|
18
|
+
RawProfileData,
|
|
19
|
+
} from '../../types/profiler.js';
|
|
20
|
+
|
|
21
|
+
interface CallFrame {
|
|
22
|
+
columnNumber: number;
|
|
23
|
+
functionName: string;
|
|
24
|
+
lineNumber: number;
|
|
25
|
+
scriptId: string;
|
|
26
|
+
url: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Chrome DevTools CPU profile format
|
|
31
|
+
*/
|
|
32
|
+
interface CpuProfile {
|
|
33
|
+
endTime: number;
|
|
34
|
+
nodes: ProfileNode[];
|
|
35
|
+
samples?: number[];
|
|
36
|
+
startTime: number;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
interface ProfileNode {
|
|
40
|
+
callFrame: CallFrame;
|
|
41
|
+
children?: number[];
|
|
42
|
+
hitCount?: number;
|
|
43
|
+
id: number;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Parse a CPU profile file
|
|
48
|
+
*
|
|
49
|
+
* @param profilePath - Path to *.cpuprofile file
|
|
50
|
+
* @returns Parsed profile data
|
|
51
|
+
*/
|
|
52
|
+
export const parseProfile = async (
|
|
53
|
+
profilePath: string,
|
|
54
|
+
): Promise<RawProfileData> => {
|
|
55
|
+
// Read and parse JSON profile
|
|
56
|
+
const content = await readFile(profilePath, 'utf-8');
|
|
57
|
+
const profile: CpuProfile = JSON.parse(content) as CpuProfile;
|
|
58
|
+
|
|
59
|
+
// Extract functions and calculate statistics
|
|
60
|
+
return parseCpuProfile(profile, profilePath);
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Parse CPU profile data structure
|
|
65
|
+
*/
|
|
66
|
+
const parseCpuProfile = (
|
|
67
|
+
profile: CpuProfile,
|
|
68
|
+
profilePath: string,
|
|
69
|
+
): RawProfileData => {
|
|
70
|
+
const functions: ProfiledFunction[] = [];
|
|
71
|
+
let totalTicks = 0;
|
|
72
|
+
|
|
73
|
+
// Calculate total ticks from all node hit counts
|
|
74
|
+
for (const node of profile.nodes) {
|
|
75
|
+
totalTicks += node.hitCount || 0;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Extract function information from nodes with hit counts
|
|
79
|
+
for (const node of profile.nodes) {
|
|
80
|
+
const hitCount = node.hitCount || 0;
|
|
81
|
+
if (hitCount === 0) {
|
|
82
|
+
continue; // Skip nodes with no samples
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const { callFrame } = node;
|
|
86
|
+
const percentage = totalTicks > 0 ? (hitCount / totalTicks) * 100 : 0;
|
|
87
|
+
|
|
88
|
+
// Parse file path from URL
|
|
89
|
+
const filePath = parseFileUrl(callFrame.url);
|
|
90
|
+
|
|
91
|
+
// Determine category based on URL
|
|
92
|
+
const category = determineCategory(callFrame.url);
|
|
93
|
+
|
|
94
|
+
functions.push({
|
|
95
|
+
category,
|
|
96
|
+
file: filePath,
|
|
97
|
+
line: callFrame.lineNumber >= 0 ? callFrame.lineNumber + 1 : null, // DevTools uses 0-based line numbers
|
|
98
|
+
name: callFrame.functionName || '(anonymous)',
|
|
99
|
+
percentage,
|
|
100
|
+
ticks: hitCount,
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Calculate summary statistics
|
|
105
|
+
const javascriptTicks = functions
|
|
106
|
+
.filter((fn) => fn.category === 'JavaScript')
|
|
107
|
+
.reduce((sum, fn) => sum + fn.ticks, 0);
|
|
108
|
+
|
|
109
|
+
const cppTicks = functions
|
|
110
|
+
.filter((fn) => fn.category === 'C++')
|
|
111
|
+
.reduce((sum, fn) => sum + fn.ticks, 0);
|
|
112
|
+
|
|
113
|
+
const summary: ProfileSummary = {
|
|
114
|
+
cppTicks,
|
|
115
|
+
gcTicks: 0, // CPU profiles don't separate GC
|
|
116
|
+
javascriptTicks,
|
|
117
|
+
sharedLibraryTicks: 0,
|
|
118
|
+
totalTicks,
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
return {
|
|
122
|
+
functions,
|
|
123
|
+
logPath: profilePath,
|
|
124
|
+
summary,
|
|
125
|
+
totalTicks,
|
|
126
|
+
};
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Parse file URL from call frame
|
|
131
|
+
*/
|
|
132
|
+
const parseFileUrl = (url: string): string => {
|
|
133
|
+
if (!url) {
|
|
134
|
+
return '<unknown>';
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Handle file:// URLs
|
|
138
|
+
if (url.startsWith('file://')) {
|
|
139
|
+
try {
|
|
140
|
+
return fileURLToPath(url);
|
|
141
|
+
} catch {
|
|
142
|
+
// Fallback if URL is malformed
|
|
143
|
+
return path.normalize(url.replace('file://', ''));
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Handle node: internal modules
|
|
148
|
+
if (url.startsWith('node:')) {
|
|
149
|
+
return url;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Handle plain paths
|
|
153
|
+
return path.normalize(url);
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Determine function category based on URL
|
|
158
|
+
*/
|
|
159
|
+
const determineCategory = (
|
|
160
|
+
url: string,
|
|
161
|
+
): 'C++' | 'GC' | 'JavaScript' | 'Unknown' => {
|
|
162
|
+
if (!url) {
|
|
163
|
+
return 'Unknown';
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Node.js internals and native modules
|
|
167
|
+
if (url.startsWith('node:') || url.includes('[native code]')) {
|
|
168
|
+
return 'C++';
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// eval'd code is JavaScript
|
|
172
|
+
if (url === '[eval]') {
|
|
173
|
+
return 'JavaScript';
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// JavaScript files
|
|
177
|
+
if (
|
|
178
|
+
url.endsWith('.js') ||
|
|
179
|
+
url.endsWith('.mjs') ||
|
|
180
|
+
url.endsWith('.cjs') ||
|
|
181
|
+
url.endsWith('.ts') ||
|
|
182
|
+
url.endsWith('.mts') ||
|
|
183
|
+
url.endsWith('.cts')
|
|
184
|
+
) {
|
|
185
|
+
return 'JavaScript';
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Default to JavaScript for file:// URLs
|
|
189
|
+
if (url.startsWith('file://')) {
|
|
190
|
+
return 'JavaScript';
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
return 'Unknown';
|
|
194
|
+
};
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Profile Runner Service
|
|
3
|
+
*
|
|
4
|
+
* Executes commands with Node.js CPU profiling enabled and captures profiler
|
|
5
|
+
* output to *.cpuprofile files in .modestbench/profiles/.
|
|
6
|
+
*
|
|
7
|
+
* @packageDocumentation
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { glob } from 'glob';
|
|
11
|
+
import { spawn } from 'node:child_process';
|
|
12
|
+
import { mkdir, stat } from 'node:fs/promises';
|
|
13
|
+
import { join } from 'node:path';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Options for running with profiling
|
|
17
|
+
*/
|
|
18
|
+
interface RunOptions {
|
|
19
|
+
/** Working directory */
|
|
20
|
+
cwd?: string;
|
|
21
|
+
|
|
22
|
+
/** Environment variables */
|
|
23
|
+
env?: NodeJS.ProcessEnv;
|
|
24
|
+
|
|
25
|
+
/** Timeout in milliseconds */
|
|
26
|
+
timeout?: number;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Run a command with Node.js profiling enabled
|
|
31
|
+
*
|
|
32
|
+
* @param command - Command to run (e.g., "npm test")
|
|
33
|
+
* @param options - Execution options
|
|
34
|
+
* @returns Path to generated *.cpuprofile file
|
|
35
|
+
*/
|
|
36
|
+
export const runWithProfiling = async (
|
|
37
|
+
command: string,
|
|
38
|
+
options: RunOptions = {},
|
|
39
|
+
): Promise<string> => {
|
|
40
|
+
const cwd = options.cwd || process.cwd();
|
|
41
|
+
|
|
42
|
+
// Create profiles directory
|
|
43
|
+
const profilesDir = join(cwd, '.modestbench', 'profiles');
|
|
44
|
+
await mkdir(profilesDir, { recursive: true });
|
|
45
|
+
|
|
46
|
+
// Run command with NODE_OPTIONS="--cpu-prof --cpu-prof-dir=..."
|
|
47
|
+
const proc = spawn(command, {
|
|
48
|
+
cwd,
|
|
49
|
+
env: {
|
|
50
|
+
...process.env,
|
|
51
|
+
...options.env,
|
|
52
|
+
NODE_OPTIONS: `--cpu-prof --cpu-prof-dir=${profilesDir}`,
|
|
53
|
+
},
|
|
54
|
+
shell: true,
|
|
55
|
+
stdio: 'inherit',
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
// Wait for process to complete
|
|
59
|
+
await new Promise<void>((resolve, reject) => {
|
|
60
|
+
const timeout = options.timeout
|
|
61
|
+
? setTimeout(() => {
|
|
62
|
+
proc.kill();
|
|
63
|
+
reject(
|
|
64
|
+
new Error(`Profile command timed out after ${options.timeout}ms`),
|
|
65
|
+
);
|
|
66
|
+
}, options.timeout)
|
|
67
|
+
: null;
|
|
68
|
+
|
|
69
|
+
proc.on('close', (code) => {
|
|
70
|
+
if (timeout) {
|
|
71
|
+
clearTimeout(timeout);
|
|
72
|
+
}
|
|
73
|
+
if (code === 0 || code === null) {
|
|
74
|
+
resolve();
|
|
75
|
+
} else {
|
|
76
|
+
reject(new Error(`Profile command exited with code ${code}`));
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
proc.on('error', (err) => {
|
|
81
|
+
if (timeout) {
|
|
82
|
+
clearTimeout(timeout);
|
|
83
|
+
}
|
|
84
|
+
reject(err);
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
// Find generated *.cpuprofile file in profiles directory
|
|
89
|
+
const profileFiles = await glob('*.cpuprofile', { cwd: profilesDir });
|
|
90
|
+
|
|
91
|
+
if (profileFiles.length === 0) {
|
|
92
|
+
throw new Error(
|
|
93
|
+
'No CPU profile generated. Ensure the command runs Node.js code.',
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Return most recent profile file
|
|
98
|
+
return await getMostRecentFile(profileFiles, profilesDir);
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Get the most recently modified file from a list
|
|
103
|
+
*/
|
|
104
|
+
const getMostRecentFile = async (
|
|
105
|
+
files: string[],
|
|
106
|
+
cwd: string,
|
|
107
|
+
): Promise<string> => {
|
|
108
|
+
let mostRecent = files[0];
|
|
109
|
+
let mostRecentTime = (await stat(`${cwd}/${mostRecent}`)).mtimeMs;
|
|
110
|
+
|
|
111
|
+
for (const file of files.slice(1)) {
|
|
112
|
+
const filePath = `${cwd}/${file}`;
|
|
113
|
+
const fileTime = (await stat(filePath)).mtimeMs;
|
|
114
|
+
if (fileTime > mostRecentTime) {
|
|
115
|
+
mostRecent = file;
|
|
116
|
+
mostRecentTime = fileTime;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return `${cwd}/${mostRecent}`;
|
|
121
|
+
};
|