// CPU Microarchitecture Recipe Definition

// @ts-check
let tool_name = 'neoprof';
let tool_version = '1.1.0';

/**
 * @type {import("./docs/jsdocs").Recipe}
 */
var recipe = {
  name: 'cpu_microarchitecture',
  title: 'CPU Microarchitecture',
  version: '1.0',
  api_version: '1.0.0',
  description:
    'The CPU Microarchitecture recipe helps you explore Arm CPU performance step by step. It gives you a broad, structured view of any bottlenecks and hotspots. You can then focus on the areas where optimization will have the most impact.',
  deployments: [
    {
      appliesTo: [{ architecture: 'aarch64', os: 'Linux' }],
      dependencies: [
        {
          type: 'tool',
          name: tool_name,
          version: tool_version,
          requiredWhen: { type: 'always' },
        },
      ],
    },
  ],
  parameters: [
    {
      id: 'metrics_group',
      required: false,
      label: 'Metrics Groups',
      description:
        'Select the metrics groups to use for the CPU Microarchitecture analysis. If not specified, all groups will be collected. Run recipe info to determine available groups on a target.',
      config: {
        type: 'select',
        options: computeValidValues,
        defaultValue: [],
      },
    },
    {
      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: 'metric_mode',
      required: false,
      label: 'Metric Mode',
      description:
        "Select the metric mode. 'ebs' is recommended due to its lower probe effect, auto will default to strobing if supported, otherwise fall back to ebs.",
      config: {
        type: 'radio',
        options: ['auto', 'ebs', 'strobing'],
        defaultValue: 'ebs',
      },
    },
    {
      id: 'collect_all',
      required: false,
      label: 'Collect all metrics',
      description:
        'If enabled, all metrics groups available on the target system will be collected. This is useful for broad analysis but may decrease individual group detail due to multiplexing.',
      config: {
        type: 'checkbox',
        defaultValue: false,
      },
    },
    {
      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 cpu_microarchitecture recipe with the given metric groups',
      exec: readyCPUMicroarchitecture,
    },
  ],
  runStages: [
    {
      name: 'Collecting metric group(s)',
      description:
        'This stage collects metric group(s) on the target and processes the captured data',
      exec: runCPUMicroarchitecture,
    },
  ],
  renderStages: [
    {
      name: 'Creating render',
      description:
        'Create the renderer specs that are used to produce visualizations',
      exec: renderCPUMicroarchitecture,
    },
  ],
};

const CPUToPlatformTelemetryFileName = {
  'Neoverse-N1': 'neoverse-n1.json',
  'Neoverse-N2': 'neoverse-n2.json',
  'Neoverse-N3': 'neoverse-n3.json',
  'Neoverse-V1': 'neoverse-v1.json',
  'Neoverse-V2': 'neoverse-v2.json',
  'Neoverse-V3': 'neoverse-v3.json',
  'Neoverse-V3AE': 'neoverse-v3.json',
};

/**
 * @param {string} metricsGroup
 * @param {string} samplingFreq
 * @param {import("./docs/jsdocs").Workload} workload
 * @param {string} metricMode
 */
function getToolsArg(metricsGroup, samplingFreq, workload, metricMode) {
  let toolsArg = {
    tools: [
      {
        name: tool_name,
        args: [
          '-M',
          metricsGroup,
          '-r',
          samplingFreq,
          '--metric-mode',
          metricMode,
        ],
      },
    ],
    workload: workload,
  };

  // Only use "-I poll" if not system-wide profiling
  // Alternatives are (-S yes), or no inherit (-I no).
  if (workload.Type != 'systemWide') {
    toolsArg.tools[0].args.push('-I', 'poll');
  }
  return toolsArg;
}

/**
 * @param {import("./docs/jsdocs").ReadyExecutionContext | import("./docs/jsdocs").RunExecutionContext} context
 */
function getMetricsGroup(context) {
  let metricsGroup = context.getParameter('metrics_group').join(',');

  // If no metrics group is specified, or collect_all is enabled, collect all groups
  if (metricsGroup.length === 0 || context.getParameter('collect_all')) {
    let platform =
      CPUToPlatformTelemetryFileName[context.targetInfo().PrimaryCPUName];
    if (!platform) {
      // Default to Neoverse-N1 if the platform is unknown
      context.logWarn(
        `Unknown CPU "${context.targetInfo().PrimaryCPUName}" defaulting to Neoverse-N1`,
      );
      platform = CPUToPlatformTelemetryFileName['Neoverse-N1'];
    }

    const pmuSpec = JSON.parse(
      context.readHostFile(`$ATPERF_DATA/${platform}`),
    );
    let allMetricsGroups = Object.values(
      pmuSpec.methodologies.topdown_methodology.metric_grouping,
    ).flat();

    // sl-collect expects metrics groups to be lower case
    metricsGroup = allMetricsGroups.join(',');
  }
  metricsGroup = metricsGroup.toLowerCase();
  context.logInfo(`collecting metrics ${metricsGroup}`);

  return metricsGroup;
}

/**
 * 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_name,
        params: params,
        workload: workload,
        env: {},
      },
    ],
  };
}

/**
 * @param {import("./docs/jsdocs").ReadyExecutionContext} context
 */
function readyCPUMicroarchitecture(context) {
  let workload = context.getWorkload();
  let samplingFreq = context.getParameter('sampling_freq');
  let metricsGroup = getMetricsGroup(context);
  let params = {
    mode: 'metrics',
    metrics_group: metricsGroup,
    sampling_frequency: samplingFreq,
    metric_mode: context.getParameter('metric_mode'),
    collect_java_stacks: context.getParameter('collect_java_stacks'),
    collect_dotnet_stacks: context.getParameter('collect_dotnet_stacks'),
  };

  let tools = generateNeoprofConfig(workload, params);
  let toolResponses = context.probeTools(tools);

  let allAdvice = 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,
    })),
  );

  return {
    status: toolStatusToRecipeStatus(allAdvice),
    advice: allAdvice,
  };
}

/**
 * @param {import("./docs/jsdocs").ReadyExecutionContext} context
 */
function computeValidValues(context) {
  let platform =
    CPUToPlatformTelemetryFileName[context.targetInfo().PrimaryCPUName];
  if (!platform) {
    // Default to Neoverse-N1 if the platform is unknown
    context.logWarn(
      `Unknown CPU "${context.targetInfo().PrimaryCPUName}" defaulting to Neoverse-N1`,
    );
    platform = CPUToPlatformTelemetryFileName['Neoverse-N1'];
  }

  const pmuSpec = JSON.parse(context.readHostFile(`$ATPERF_DATA/${platform}`));

  // Generate a list of unique metric groups from pmuSpec.methodologies.topdown_methodology.decision_tree.metrics
  // Include metrics.group and all items in metrics.next_items, as the main group is usually Topdown_L1,
  // which is also a valid choice.
  // Not using pmuSpec.methodologies.topdown_methodology.metric_grouping as it includes groups not in the decision tree
  // which do not display in the GUI summary view.
  let all_metrics_groups = new Set();
  for (let item of pmuSpec.methodologies.topdown_methodology.decision_tree
    .metrics) {
    all_metrics_groups.add(item.group);
    for (let next_item of item.next_items) {
      all_metrics_groups.add(next_item);
    }
  }
  all_metrics_groups = Array.from(all_metrics_groups);
  all_metrics_groups.sort();

  // sl-collect expects metrics groups to be lower case
  return all_metrics_groups.map((metric_group) => metric_group.toLowerCase());
}

/**
 * @param {import("./docs/jsdocs").RunExecutionContext} context
 */
function runCPUMicroarchitecture(context) {
  const sampling_freq = context.getParameter('sampling_freq');
  const workload = context.getWorkload();
  let metrics_group = getMetricsGroup(context);
  let params = {
    mode: 'metrics',
    metrics_group: metrics_group,
    sampling_frequency: sampling_freq,
    metric_mode: context.getParameter('metric_mode'),
    collect_java_stacks: context.getParameter('collect_java_stacks'),
    collect_dotnet_stacks: context.getParameter('collect_dotnet_stacks'),
  };
  context.runTools(generateNeoprofConfig(workload, params));
}

/**
 * @param {import("./docs/jsdocs").RenderExecutionContext} context
 */
function renderCPUMicroarchitecture(context) {
  const isComparison = context.getRunDescriptions().length === 2;
  let renderers = [];
  let visualizations = [];

  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 = {
    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,
        },
      ],
    },
  };

  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,
        },
      ],
    },
  };

  if (context.isRerenderingEnabled()) {
    const filterPid = Number(context.getRenderParameter('filter_pid'));
    const filterTid = Number(context.getRenderParameter('filter_tid'));

    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,
      },
      {
        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',
    },
    {
      type: 'StreamlineAnalyzeFlatFunctions2',
      id: 'flat',
      config: {
        'compute-metrics': [
          {
            type: 'percentage',
            'total-from': 'Sample Count (self)',
            columns: ['Sample Count (self)'],
            'relative-order-priority': 'higher',
          },
        ],
        data_source: dataSource,
        entity: `tool/${tool_name}/0/`,
      },
    },
    {
      type: 'StreamlineAnalyzeFunctionProfileRenderer2',
      id: 'drilldown',
      config: {
        data_source: dataSource,
        entity: `tool/${tool_name}/0/`,
      },
    },
    {
      type: 'SourceCodeAttribution',
      id: 'source_code_attribution',
      config: {
        data_source: sourceFiles,
        entity: `tool/${tool_name}/0/`,
      },
    },
  );
  visualizations.push(
    isComparison
      ? {
          type: 'topdown_node_graph_comparison',
          id: 'node_graph',
          rendererId: 'compare_drilldown',
          title: 'Summary Comparison',
          description:
            'Compare changes in the frontend, backend, retiring, and bad speculation metrics between the current run and the baseline.',
          config: {
            data_source: {
              tables: {
                callstackDeltas: [
                  { renderer_id: 'compare_drilldown', output: 'delta' },
                ],
                callstackMeasurements: [
                  { renderer_id: 'drilldown', output: 'measurements' },
                ],
                callstackMeasurementOrder: [
                  { renderer_id: 'drilldown', output: 'measurement_order' },
                ],
                flatFunctionsCurrentRun: [
                  {
                    renderer_id: 'flat',
                    output: 'drilldown',
                    content_index: 1,
                  },
                ],
                flatFunctionsBaselineRun: [
                  {
                    renderer_id: 'flat',
                    output: 'drilldown',
                    content_index: 0,
                  },
                ],
                flatFunctionsDeltas: [
                  { renderer_id: 'compare_flat', output: 'delta_flat' },
                ],
                flatFunctionsMeasurements: [
                  { renderer_id: 'flat', output: 'measurements' },
                ],
                flatFunctionsMeasurementOrder: [
                  { 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,
                  },
                ],
              },
            },
          },
        }
      : {
          type: 'topdown_node_graph',
          id: 'node_graph',
          rendererId: 'drilldown',
          title: 'Summary',
          description:
            'View a summary of frontend, backend, retiring, and bad speculation metrics.',
          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' }],
                flatFunctionsMeasurements: [
                  { renderer_id: 'flat', output: 'measurements' },
                ],
                flatFunctionsMeasurementOrder: [
                  { renderer_id: 'flat', output: 'measurement_order' },
                ],
                symbols: [
                  {
                    renderer_id: 'streamline_symbols',
                    output: 'symbols',
                  },
                ],
                images: [
                  {
                    renderer_id: 'streamline_symbols',
                    output: 'images',
                  },
                ],
              },
            },
          },
        },
    isComparison
      ? {
          type: 'flat_functions_comparison',
          id: 'functions',
          rendererId: 'compare_flat',
          title: 'Functions Comparison',
          description:
            'Comparison of how much CPU time different functions comsume, and of other per-function performance metrics.',
          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',
                  },
                ],
              },
            },
          },
        },
    isComparison
      ? {
          type: 'call_stack_comparison',
          id: 'call_stack',
          rendererId: 'compare_drilldown',
          title: 'Call Stack Comparison',
          description:
            'Compare performance metrics grouped by call path 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,
                  },
                ],
                source_files: [
                  {
                    renderer_id: 'streamline_symbols',
                    output: 'source_files',
                  },
                ],
              },
            },
          },
        }
      : {
          type: 'call_stack',
          id: 'call_stack',
          rendererId: 'drilldown',
          title: 'Call Stack',
          description:
            'Investigate performance metrics grouped by call path. This information helps you analyze where and how functions are used during execution as well as their costs.',
          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',
                  },
                ],
              },
            },
          },
        },
  );

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