// Code Hotspots Recipe Definition

// @ts-check
const TOOL_NEOPROF = { name: 'neoprof', version: '1.1.0' };
const TOOL_WPERF = { name: 'wperf', version: '0.1.0' };
const readinessMessageCode =
  'engine.recipeparser.js_recipe_stage.READINESS_MESSAGE';

/**
 * @type {import("./docs/jsdocs").Recipe}
 */
var recipe = {
  name: 'code_hotspots',
  title: 'Code Hotspots',
  version: '1.0',
  api_version: '1.0.0',
  description:
    'The Code Hotspots recipe shows which parts of your code consume the most CPU time. It helps you quickly find and fix performance bottlenecks by identifying the functions and lines where optimization will have the most impact.',
  deployments: [
    {
      appliesTo: [{ architecture: 'aarch64', os: 'Windows' }],
      dependencies: [
        {
          type: 'tool',
          name: TOOL_WPERF.name,
          version: TOOL_WPERF.version,
          requiredWhen: { type: 'always' },
        },
      ],
    },
    {
      appliesTo: [
        { architecture: 'aarch64', os: 'Linux' },
        { architecture: 'x86_64', os: 'Linux' },
      ],
      dependencies: [
        {
          type: 'tool',
          name: TOOL_NEOPROF.name,
          version: TOOL_NEOPROF.version,
          requiredWhen: { type: 'always' },
        },
      ],
    },
  ],
  parameters: [
    {
      id: 'sampling_freq',
      required: false,
      label: 'Sampling Frequency',
      description:
        "Select the sampling frequency. The 'normal' frequency is suitable for most workloads, while 'high' provides more detailed information at the cost of increased overhead.",
      config: {
        type: 'radio',
        options: ['low', 'normal', 'high'],
        defaultValue: 'normal',
      },
    },
    {
      id: 'collect_java_stacks',
      required: false,
      label: 'Collect Java stacks',
      description:
        'Enable collection of Java stack traces when profiling JVM workloads.',
      config: {
        type: 'checkbox',
        defaultValue: false,
      },
    },
    {
      id: 'collect_dotnet_stacks',
      required: false,
      label: 'Collect .NET stacks',
      description:
        'Enable collection of .NET stack traces when profiling .NET workloads.',
      config: {
        type: 'checkbox',
        defaultValue: false,
      },
    },
  ],
  renderParameters: [
    {
      id: 'filter_pid',
      required: false,
      label: 'Filter PID',
      description: 'Optional filter for a specific PID.',
      config: {
        type: 'input',
        defaultValue: '0',
      },
    },
    {
      id: 'filter_tid',
      required: false,
      label: 'Filter TID',
      description: 'Optional filter for a specific TID.',
      config: {
        type: 'input',
        defaultValue: '0',
      },
    },
  ],
  readyStages: [
    {
      name: 'Checking recipe is ready',
      description: 'Check that the target can run the Code Hotspots recipe',
      exec: readyHotspots,
    },
  ],
  runStages: [
    {
      name: 'Collecting hotspots data',
      description:
        'This stage collects hotspot samples on the target and processes the captured data',
      exec: runHotspots,
    },
  ],
  renderStages: [
    {
      name: 'Creating render',
      description:
        'Create the renderer specs that are used to produce visualizations',
      exec: renderHotspots,
    },
  ],
};

/**
 * @param {string} samplingFreq
 * @param {import("./docs/jsdocs").Workload} workload
 */
function getToolsArg(samplingFreq, workload) {
  return {
    tools: [
      {
        name: TOOL_NEOPROF.name,
        args: ['-r', samplingFreq],
      },
    ],
    workload: workload,
  };
}

/**
 * toolStatusToRecipeStatus converts recipe ready advice to a single
 * recipe ready status. The most severe is returned.
 * @param {import("./docs/jsdocs").RecipeReadyAdvice[]} advice
 * @returns {string} readyStatus
 */
function toolStatusToRecipeStatus(advice) {
  const rank = { ready: 0, unknown: 1, warning: 2, error: 3 };
  return advice.reduce(
    (worst, { AdviceSeverity }) =>
      rank[AdviceSeverity] > rank[worst] ? AdviceSeverity : worst,
    'ready',
  );
}

/**
 * generateNeoprofConfig generates a ToolConfigurationsArg for the
 * neoprof tool integration.
 * @param {import("./docs/jsdocs").Workload} workload
 * @param {Object.<string, any>} params
 * @return {import("./docs/jsdocs").ToolConfigurationsArg}
 */
function generateNeoprofConfig(workload, params) {
  return {
    toolConfigs: [
      {
        name: TOOL_NEOPROF.name,
        params: params,
        workload: workload,
        env: {},
      },
    ],
  };
}

/**
 * generateWperfConfig generates a ToolConfigurationsArg for the
 * wperf tool integration.
 * @param {import("./docs/jsdocs").Workload} workload
 * @param {Object.<string, any>} params
 * @return {import("./docs/jsdocs").ToolConfigurationsArg}
 */
function generateWperfConfig(workload, params) {
  return {
    toolConfigs: [
      {
        name: TOOL_WPERF.name,
        params: params,
        workload: workload,
        env: {},
      },
    ],
  };
}

/**
 * Build the neoprof parameter set used by agent-mode invocations.
 * @param {import("./docs/jsdocs").RunExecutionContext|import("./docs/jsdocs").ReadyExecutionContext} context
 * @param {string} samplingFreq
 * @returns {Object.<string, any>}
 */
function buildNeoprofParams(context, samplingFreq) {
  return {
    mode: 'samples',
    sampling_frequency: samplingFreq,
    collect_java_stacks: context.getParameter('collect_java_stacks'),
    collect_dotnet_stacks: context.getParameter('collect_dotnet_stacks'),
  };
}

/**
 * Build the wperf parameter set used by agent-mode invocations.
 * @param {string} samplingFreq
 * @returns {Object.<string, any>}
 */
function buildWperfParams(samplingFreq) {
  return {
    mode: 'samples',
    sampling_frequency: samplingFreq ? samplingFreq : 'normal',
  };
}

/**
 * Determine whether the current target OS is Windows.
 * @param {import("./docs/jsdocs").TargetInfoDescription} targetInfo
 * @returns {boolean}
 */
function isWindowsTarget(targetInfo) {
  const family = targetInfo?.Os?.OSFamily ?? '';
  return family.toLowerCase() === 'windows';
}

/**
 * Normalize advice returned by tool probes/runs.
 * @param {import("./docs/jsdocs").ToolConfigurationsArg} tools
 * @param {import("./docs/jsdocs").ProbeResult[]} toolResponses
 * @returns {import("./docs/jsdocs").RecipeReadyAdvice[]}
 */
function collectToolAdvice(tools, toolResponses) {
  return toolResponses.flatMap((tr, i) =>
    (tr.advice ?? []).map((a) => ({
      ToolName: tools.toolConfigs[i].name,
      AdviceSeverity: a.level,
      MessageCode: a.messageCode,
      Metadata: a.metadata,
      Cause: a.cause,
    })),
  );
}

/**
 * Build a ready-stage response indicating agent mode is required.
 * @param {string} toolName
 * @returns {import("./docs/jsdocs").RecipeReadyOutput}
 */
function buildAgentRequiredResponse(toolName) {
  return {
    status: 'error',
    advice: [
      {
        ToolName: toolName,
        AdviceSeverity: 'error',
        MessageCode: readinessMessageCode,
        Metadata: {
          message:
            'The Code Hotspots recipe requires agent mode when targeting Windows.',
        },
      },
    ],
  };
}

/**
 * @param {import("./docs/jsdocs").ReadyExecutionContext} context
 */
function readyHotspots(context) {
  const workload = context.getWorkload();
  const samplingFreq = context.getParameter('sampling_freq');
  const targetInfo = context.targetInfo();
  const windowsTarget = isWindowsTarget(targetInfo);

  if (windowsTarget) {
    const tools = generateWperfConfig(workload, buildWperfParams(samplingFreq));
    const toolResponses = context.probeTools(tools);
    const allAdvice = collectToolAdvice(tools, toolResponses);
    return {
      status: toolStatusToRecipeStatus(allAdvice),
      advice: allAdvice,
    };
  }

  const params = buildNeoprofParams(context, samplingFreq);
  const tools = generateNeoprofConfig(workload, params);
  const toolResponses = context.probeTools(tools);
  const allAdvice = collectToolAdvice(tools, toolResponses);
  return {
    status: toolStatusToRecipeStatus(allAdvice),
    advice: allAdvice,
  };
}

/**
 * @param {import("./docs/jsdocs").RunExecutionContext} context
 */
function runHotspots(context) {
  const samplingFreq = context.getParameter('sampling_freq');
  const workload = context.getWorkload();
  const targetInfo = context.targetInfo();
  const windowsTarget = isWindowsTarget(targetInfo);

  if (windowsTarget) {
    context.runTools(generateWperfConfig(workload, buildWperfParams()));
    return;
  }

  context.runTools(
    generateNeoprofConfig(workload, buildNeoprofParams(context, samplingFreq)),
  );
}

/**
 * @param {import("./docs/jsdocs").RenderExecutionContext} context
 * @return {{ tool: { name: string, version: string } | null, errorDescription: string | null }}
 */
function getRenderRunTool(context) {
  let result = { tool: null, errorDescription: null };
  const runDescriptions = context.getRunDescriptions();

  for (const runDesc of runDescriptions) {
    // If this is a legacy run where ToolsUsed is null - assume neoprof, else get the tool used.
    let currToolDescr = TOOL_NEOPROF;
    for (const toolName of runDesc.ToolsUsed ?? []) {
      if (toolName === TOOL_NEOPROF.name) {
        currToolDescr = TOOL_NEOPROF;
      } else if (toolName === TOOL_WPERF.name) {
        currToolDescr = TOOL_WPERF;
      } else {
        return {
          tool: null,
          errorDescription: `Unsupported tool "${toolName}" used in run - cannot render.`,
        };
      }
    }

    if (!result.tool) {
      result = { tool: currToolDescr, errorDescription: null };
    } else if (result.tool.name !== currToolDescr.name) {
      // Mixed tools in comparison - cannot render.
      return {
        tool: null,
        errorDescription: 'Mixed tools in comparison - cannot render.',
      };
    }
  }

  return result.tool ? result : { tool: TOOL_NEOPROF, errorDescription: null };
}

/**
 * @param {import("./docs/jsdocs").RenderExecutionContext} context
 */
function renderHotspots(context) {
  const isComparison = context.getRunDescriptions().length === 2;
  const runToolInfo = getRenderRunTool(context);

  if (runToolInfo.errorDescription) {
    throw {
      code: 'cli.cmd.run.render.RENDERER_FAILED',
      metadata: { failures: runToolInfo.errorDescription },
    };
  }

  const tool = runToolInfo.tool;
  const callstacksAvailable = tool.name === TOOL_NEOPROF.name;
  const filterPid = Number(context.getRenderParameter('filter_pid'));
  const filterTid = Number(context.getRenderParameter('filter_tid'));

  const dataSourceSingle = {
    tables: {
      symbols: [{ renderer_id: 'streamline_symbols', output: 'symbols' }],
      images: [{ renderer_id: 'streamline_symbols', output: 'images' }],
      target_info_cpus: [
        { renderer_id: 'target_info', output: 'target_info_cpus' },
      ],
    },
  };

  const dataSourceComparison = {
    tables: {
      symbols: [
        {
          renderer_id: 'streamline_symbols',
          output: 'symbols',
          content_index: 0,
        },
        {
          renderer_id: 'streamline_symbols',
          output: 'symbols',
          content_index: 1,
        },
      ],
      images: [
        {
          renderer_id: 'streamline_symbols',
          output: 'images',
          content_index: 0,
        },
        {
          renderer_id: 'streamline_symbols',
          output: 'images',
          content_index: 1,
        },
      ],
      target_info_cpus: [
        {
          renderer_id: 'target_info',
          output: 'target_info_cpus',
          content_index: 0,
        },
        {
          renderer_id: 'target_info',
          output: 'target_info_cpus',
          content_index: 1,
        },
      ],
    },
  };

  const sourceFilesSingle = {
    tables: {
      source_files: [
        { renderer_id: 'streamline_symbols', output: 'source_files' },
      ],
    },
  };

  const sourceFilesComparison = {
    tables: {
      source_files: [
        {
          renderer_id: 'streamline_symbols',
          output: 'source_files',
          content_index: 0,
        },
        {
          renderer_id: 'streamline_symbols',
          output: 'source_files',
          content_index: 1,
        },
      ],
    },
  };

  const dataSource = isComparison ? dataSourceComparison : dataSourceSingle;
  const sourceFiles = isComparison ? sourceFilesComparison : sourceFilesSingle;

  const dataSourceCompareDrilldownStacks = callstacksAvailable
    ? {
        tables: {
          drilldown: [
            { renderer_id: 'drilldown', output: 'drilldown', content_index: 0 },
            { renderer_id: 'drilldown', output: 'drilldown', content_index: 1 },
          ],
          symbols: [
            {
              renderer_id: 'streamline_symbols',
              output: 'symbols',
              content_index: 0,
            },
            {
              renderer_id: 'streamline_symbols',
              output: 'symbols',
              content_index: 1,
            },
          ],
          images: [
            {
              renderer_id: 'streamline_symbols',
              output: 'images',
              content_index: 0,
            },
            {
              renderer_id: 'streamline_symbols',
              output: 'images',
              content_index: 1,
            },
          ],
        },
      }
    : null;

  const dataSourceCompareDrilldownFlat = {
    tables: {
      drilldown: [
        { renderer_id: 'flat', output: 'drilldown', content_index: 0 },
        { renderer_id: 'flat', output: 'drilldown', content_index: 1 },
      ],
      symbols: [
        {
          renderer_id: 'streamline_symbols',
          output: 'symbols',
          content_index: 0,
        },
        {
          renderer_id: 'streamline_symbols',
          output: 'symbols',
          content_index: 1,
        },
      ],
      images: [
        {
          renderer_id: 'streamline_symbols',
          output: 'images',
          content_index: 0,
        },
        {
          renderer_id: 'streamline_symbols',
          output: 'images',
          content_index: 1,
        },
      ],
    },
  };

  let renderers = [];
  // SlAnalyzeRenderer and ProcessesAndThreadsParser are only applicable to neoprof since wperf does not capture an apc dir.
  if (context.isRerenderingEnabled() && tool.name === TOOL_NEOPROF.name) {
    const slAnalyzeConfig = { entity: `tool/${tool.name}/0/` };
    if (Number.isFinite(filterPid) && filterPid > 0) {
      slAnalyzeConfig.filter_pid = filterPid;
    }
    if (Number.isFinite(filterTid) && filterTid > 0) {
      slAnalyzeConfig.filter_tid = filterTid;
    }
    renderers.push({
      type: 'SlAnalyzeRenderer',
      id: 'sl_analyze',
      config: slAnalyzeConfig,
    });
    renderers.push({
      type: 'ProcessesAndThreadsParser',
      id: 'processes_and_threads',
      config: { entity: `tool/${tool.name}/0/` },
    });
  }
  renderers.push(
    {
      type: 'StreamlineAnalyzeSymbols',
      id: 'streamline_symbols',
      config: { entity: `tool/${tool.name}/0/` },
    },
    {
      type: 'TargetInfoRenderer',
      id: 'target_info',
      config: { entity: `tool/${tool.name}/0/` },
    },
    {
      type: 'StreamlineAnalyzeFlatFunctions2',
      id: 'flat',
      config: {
        component: 'functions-capture-periodic_sampling.csv',
        'compute-metrics': [
          {
            type: 'percentage',
            'total-from': 'Periodic Samples (self)',
            columns: ['Periodic Samples (self)'],
            'relative-order-priority': 'higher',
          },
        ],
        data_source: dataSource,
        entity: `tool/${tool.name}/0/`,
      },
    },
    {
      type: 'StreamlineAnalyzeFunctionProfileRenderer2',
      id: 'drilldown',
      config: {
        'call-tree': 'call_tree_samples.json',
        entity: `tool/${tool.name}/0/`,
        measurements: [
          {
            component: 'callpath_self_samples.json',
            'column-suffix': 'self',
          },
          {
            component: 'callpath_total_samples.json',
            'column-suffix': 'total',
          },
        ],
        'compute-metrics': [
          {
            type: 'percentage',
            'total-from': 'Periodic Samples (self)',
            columns: ['Periodic Samples (self)', 'Periodic Samples (total)'],
            'relative-order-priority': 'higher',
          },
        ],
        data_source: dataSource,
      },
    },
    {
      type: 'SourceCodeAttribution',
      id: 'source_code_attribution',
      config: {
        entity: `tool/${tool.name}/0/`,
        data_source: sourceFiles,
      },
    },
  );

  let renderSpec = {
    renderers: renderers,
    visualizations: [
      ...(callstacksAvailable
        ? [
            isComparison
              ? {
                  type: 'flame_graph_comparison',
                  id: 'flame_graph',
                  rendererId: 'compare_drilldown',
                  title: 'Flame Graph Comparison',
                  description:
                    'Compare sampled stack traces between the current run and the baseline. The leaf function appears at the top of the graph, and its callers appear below it. The box width represents the number of samples. Red indicates functions sampled more often in the current run. Blue indicates functions sampled more often in the baseline. Use this view to identify call paths whose performance changed between runs.',
                  config: {
                    data_source: {
                      tables: {
                        deltas: [
                          { renderer_id: 'compare_drilldown', output: 'delta' },
                        ],
                        measurements: [
                          { renderer_id: 'drilldown', output: 'measurements' },
                        ],
                        measurementOrder: [
                          {
                            renderer_id: 'drilldown',
                            output: 'measurement_order',
                          },
                        ],
                        symbols: [
                          {
                            renderer_id: 'streamline_symbols',
                            output: 'symbols',
                            content_index: 1,
                          },
                        ],
                        images: [
                          {
                            renderer_id: 'streamline_symbols',
                            output: 'images',
                            content_index: 1,
                          },
                        ],
                      },
                    },
                  },
                }
              : {
                  type: 'flame_graph',
                  id: 'flame_graph',
                  rendererId: 'drilldown',
                  title: 'Flame Graph',
                  description:
                    'View sampled stack traces and identify hot code paths. The leaf function call appears at the top of the graph, and its callers appear below it. The box width represents how often a function appears in the samples.',
                  config: {
                    data_source: {
                      tables: {
                        callstack: [
                          { renderer_id: 'drilldown', output: 'drilldown' },
                        ],
                        callstackMeasurements: [
                          { renderer_id: 'drilldown', output: 'measurements' },
                        ],
                        callstackMeasurementOrder: [
                          {
                            renderer_id: 'drilldown',
                            output: 'measurement_order',
                          },
                        ],
                        flatFunctions: [
                          { renderer_id: 'flat', output: 'drilldown' },
                        ],
                        flatFunctionsMeasurementOrder: [
                          {
                            renderer_id: 'flat',
                            output: 'measurement_order',
                          },
                        ],
                        flatFunctionsMeasurements: [
                          { renderer_id: 'flat', output: 'measurements' },
                        ],
                        symbols: [
                          {
                            renderer_id: 'streamline_symbols',
                            output: 'symbols',
                          },
                        ],
                        images: [
                          {
                            renderer_id: 'streamline_symbols',
                            output: 'images',
                          },
                        ],
                      },
                    },
                  },
                },
          ]
        : []), // callstacksAvailable === false
      isComparison
        ? {
            type: 'flat_functions_comparison',
            id: 'functions',
            rendererId: 'compare_flat',
            title: 'Functions Comparison',
            description:
              'Compare per-function CPU time between runs to identify performance changes.',
            config: {
              data_source: {
                tables: {
                  flatFunctionsCurrentRun: [
                    {
                      renderer_id: 'flat',
                      output: 'drilldown',
                      content_index: 1,
                    },
                  ],
                  flatFunctionsBaselineRun: [
                    {
                      renderer_id: 'flat',
                      output: 'drilldown',
                      content_index: 0,
                    },
                  ],
                  deltas: [
                    { renderer_id: 'compare_flat', output: 'delta_flat' },
                  ],
                  measurements: [
                    { renderer_id: 'flat', output: 'measurements' },
                  ],
                  measurementOrder: [
                    { renderer_id: 'flat', output: 'measurement_order' },
                  ],
                  symbols: [
                    {
                      renderer_id: 'streamline_symbols',
                      output: 'symbols',
                      content_index: 1,
                    },
                  ],
                  images: [
                    {
                      renderer_id: 'streamline_symbols',
                      output: 'images',
                      content_index: 1,
                    },
                  ],
                  source_files: [
                    {
                      renderer_id: 'streamline_symbols',
                      output: 'source_files',
                    },
                  ],
                },
              },
            },
          }
        : {
            type: 'flat_functions',
            id: 'functions',
            rendererId: 'flat',
            title: 'Functions',
            description: 'Identify functions that consume the most CPU time.',
            config: {
              data_source: {
                tables: {
                  flatFunctions: [{ renderer_id: 'flat', output: 'drilldown' }],
                  measurements: [
                    { renderer_id: 'flat', output: 'measurements' },
                  ],
                  measurementOrder: [
                    { renderer_id: 'flat', output: 'measurement_order' },
                  ],
                  symbols: [
                    {
                      renderer_id: 'streamline_symbols',
                      output: 'symbols',
                    },
                  ],
                  images: [
                    { renderer_id: 'streamline_symbols', output: 'images' },
                  ],
                  source_files: [
                    {
                      renderer_id: 'streamline_symbols',
                      output: 'source_files',
                    },
                  ],
                },
              },
            },
          },
      ...(callstacksAvailable
        ? [
            isComparison
              ? {
                  type: 'call_stack_comparison',
                  id: 'call_stack',
                  rendererId: 'compare_drilldown',
                  title: 'Call Stack Comparison',
                  description:
                    'Compare CPU time performance metrics between runs, grouped by call path. Identify where execution cost shifted between runs. This view includes the function’s own execution time (self) and the time of the function and all the functions that called it (total).',
                  config: {
                    data_source: {
                      tables: {
                        deltas: [
                          { renderer_id: 'compare_drilldown', output: 'delta' },
                        ],
                        measurements: [
                          { renderer_id: 'drilldown', output: 'measurements' },
                        ],
                        measurementOrder: [
                          {
                            renderer_id: 'drilldown',
                            output: 'measurement_order',
                          },
                        ],
                        symbols: [
                          {
                            renderer_id: 'streamline_symbols',
                            output: 'symbols',
                            content_index: 1,
                          },
                        ],
                        images: [
                          {
                            renderer_id: 'streamline_symbols',
                            output: 'images',
                            content_index: 1,
                          },
                        ],
                        source_files: [
                          {
                            renderer_id: 'streamline_symbols',
                            output: 'source_files',
                          },
                        ],
                      },
                    },
                  },
                }
              : {
                  type: 'call_stack',
                  id: 'call_stack',
                  rendererId: 'drilldown',
                  title: 'Call Stack',
                  description:
                    'View CPU time information for each function grouped by call path. This view includes the function’s own execution time (self) and the time of the function and all the functions that called it (total). Use this view to determine whether execution cost originates in a function or in its call chain.',
                  config: {
                    data_source: {
                      tables: {
                        drilldown: [
                          { renderer_id: 'drilldown', output: 'drilldown' },
                        ],
                        measurements: [
                          { renderer_id: 'drilldown', output: 'measurements' },
                        ],
                        measurementOrder: [
                          {
                            renderer_id: 'drilldown',
                            output: 'measurement_order',
                          },
                        ],
                        symbols: [
                          {
                            renderer_id: 'streamline_symbols',
                            output: 'symbols',
                          },
                        ],
                        images: [
                          {
                            renderer_id: 'streamline_symbols',
                            output: 'images',
                          },
                        ],
                        source_files: [
                          {
                            renderer_id: 'streamline_symbols',
                            output: 'source_files',
                          },
                        ],
                      },
                    },
                  },
                },
          ]
        : []), // callstacksAvailable === false
    ],
  };

  if (isComparison) {
    if (callstacksAvailable) {
      renderSpec.renderers.push({
        type: 'CompareDrilldownCallStacks',
        id: 'compare_drilldown',
        config: { data_source: dataSourceCompareDrilldownStacks },
      });
    }
    renderSpec.renderers.push({
      type: 'CompareDrilldownFlat',
      id: 'compare_flat',
      config: { data_source: dataSourceCompareDrilldownFlat },
    });
  }

  return renderSpec;
}
