Path: blob/main/src/vs/platform/diagnostics/node/diagnosticsService.ts
3296 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import * as fs from 'fs';6import * as osLib from 'os';7import { Promises } from '../../../base/common/async.js';8import { getNodeType, parse, ParseError } from '../../../base/common/json.js';9import { Schemas } from '../../../base/common/network.js';10import { basename, join } from '../../../base/common/path.js';11import { isLinux, isWindows } from '../../../base/common/platform.js';12import { ProcessItem } from '../../../base/common/processes.js';13import { StopWatch } from '../../../base/common/stopwatch.js';14import { URI } from '../../../base/common/uri.js';15import { virtualMachineHint } from '../../../base/node/id.js';16import { IDirent, Promises as pfs } from '../../../base/node/pfs.js';17import { listProcesses } from '../../../base/node/ps.js';18import { IDiagnosticsService, IMachineInfo, IMainProcessDiagnostics, IRemoteDiagnosticError, IRemoteDiagnosticInfo, isRemoteDiagnosticError, IWorkspaceInformation, PerformanceInfo, SystemInfo, WorkspaceStatItem, WorkspaceStats } from '../common/diagnostics.js';19import { ByteSize } from '../../files/common/files.js';20import { IProductService } from '../../product/common/productService.js';21import { ITelemetryService } from '../../telemetry/common/telemetry.js';22import { IWorkspace } from '../../workspace/common/workspace.js';2324interface ConfigFilePatterns {25tag: string;26filePattern: RegExp;27relativePathPattern?: RegExp;28}2930const workspaceStatsCache = new Map<string, Promise<WorkspaceStats>>();31export async function collectWorkspaceStats(folder: string, filter: string[]): Promise<WorkspaceStats> {32const cacheKey = `${folder}::${filter.join(':')}`;33const cached = workspaceStatsCache.get(cacheKey);34if (cached) {35return cached;36}3738const configFilePatterns: ConfigFilePatterns[] = [39{ tag: 'grunt.js', filePattern: /^gruntfile\.js$/i },40{ tag: 'gulp.js', filePattern: /^gulpfile\.js$/i },41{ tag: 'tsconfig.json', filePattern: /^tsconfig\.json$/i },42{ tag: 'package.json', filePattern: /^package\.json$/i },43{ tag: 'jsconfig.json', filePattern: /^jsconfig\.json$/i },44{ tag: 'tslint.json', filePattern: /^tslint\.json$/i },45{ tag: 'eslint.json', filePattern: /^eslint\.json$/i },46{ tag: 'tasks.json', filePattern: /^tasks\.json$/i },47{ tag: 'launch.json', filePattern: /^launch\.json$/i },48{ tag: 'mcp.json', filePattern: /^mcp\.json$/i },49{ tag: 'settings.json', filePattern: /^settings\.json$/i },50{ tag: 'webpack.config.js', filePattern: /^webpack\.config\.js$/i },51{ tag: 'project.json', filePattern: /^project\.json$/i },52{ tag: 'makefile', filePattern: /^makefile$/i },53{ tag: 'sln', filePattern: /^.+\.sln$/i },54{ tag: 'csproj', filePattern: /^.+\.csproj$/i },55{ tag: 'cmake', filePattern: /^.+\.cmake$/i },56{ tag: 'github-actions', filePattern: /^.+\.ya?ml$/i, relativePathPattern: /^\.github(?:\/|\\)workflows$/i },57{ tag: 'devcontainer.json', filePattern: /^devcontainer\.json$/i },58{ tag: 'dockerfile', filePattern: /^(dockerfile|docker\-compose\.ya?ml)$/i },59{ tag: 'cursorrules', filePattern: /^\.cursorrules$/i },60{ tag: 'cursorrules-dir', filePattern: /\.mdc$/i, relativePathPattern: /^\.cursor[\/\\]rules$/i },61{ tag: 'github-instructions-dir', filePattern: /\.instructions\.md$/i, relativePathPattern: /^\.github[\/\\]instructions$/i },62{ tag: 'github-prompts-dir', filePattern: /\.prompt\.md$/i, relativePathPattern: /^\.github[\/\\]prompts$/i },63{ tag: 'clinerules', filePattern: /^\.clinerules$/i },64{ tag: 'clinerules-dir', filePattern: /\.md$/i, relativePathPattern: /^\.clinerules$/i },65{ tag: 'agent.md', filePattern: /^agent\.md$/i },66{ tag: 'agents.md', filePattern: /^agents\.md$/i },67{ tag: 'claude.md', filePattern: /^claude\.md$/i },68{ tag: 'gemini.md', filePattern: /^gemini\.md$/i },69{ tag: 'copilot-instructions.md', filePattern: /^copilot\-instructions\.md$/i, relativePathPattern: /^\.github$/i },70];7172const fileTypes = new Map<string, number>();73const configFiles = new Map<string, number>();7475const MAX_FILES = 20000;7677function collect(root: string, dir: string, filter: string[], token: { count: number; maxReached: boolean; readdirCount: number }): Promise<void> {78const relativePath = dir.substring(root.length + 1);7980return Promises.withAsyncBody(async resolve => {81let files: IDirent[];8283token.readdirCount++;84try {85files = await pfs.readdir(dir, { withFileTypes: true });86} catch (error) {87// Ignore folders that can't be read88resolve();89return;90}9192if (token.count >= MAX_FILES) {93token.count += files.length;94token.maxReached = true;95resolve();96return;97}9899let pending = files.length;100if (pending === 0) {101resolve();102return;103}104105let filesToRead = files;106if (token.count + files.length > MAX_FILES) {107token.maxReached = true;108pending = MAX_FILES - token.count;109filesToRead = files.slice(0, pending);110}111112token.count += files.length;113114for (const file of filesToRead) {115if (file.isDirectory()) {116if (!filter.includes(file.name)) {117await collect(root, join(dir, file.name), filter, token);118}119120if (--pending === 0) {121resolve();122return;123}124} else {125const index = file.name.lastIndexOf('.');126if (index >= 0) {127const fileType = file.name.substring(index + 1);128if (fileType) {129fileTypes.set(fileType, (fileTypes.get(fileType) ?? 0) + 1);130}131}132133for (const configFile of configFilePatterns) {134if (configFile.relativePathPattern?.test(relativePath) !== false && configFile.filePattern.test(file.name)) {135configFiles.set(configFile.tag, (configFiles.get(configFile.tag) ?? 0) + 1);136}137}138139if (--pending === 0) {140resolve();141return;142}143}144}145});146}147148const statsPromise = Promises.withAsyncBody<WorkspaceStats>(async (resolve) => {149const token: { count: number; maxReached: boolean; readdirCount: number } = { count: 0, maxReached: false, readdirCount: 0 };150const sw = new StopWatch(true);151await collect(folder, folder, filter, token);152const launchConfigs = await collectLaunchConfigs(folder);153resolve({154configFiles: asSortedItems(configFiles),155fileTypes: asSortedItems(fileTypes),156fileCount: token.count,157maxFilesReached: token.maxReached,158launchConfigFiles: launchConfigs,159totalScanTime: sw.elapsed(),160totalReaddirCount: token.readdirCount161});162});163164workspaceStatsCache.set(cacheKey, statsPromise);165return statsPromise;166}167168function asSortedItems(items: Map<string, number>): WorkspaceStatItem[] {169return Array.from(items.entries(), ([name, count]) => ({ name: name, count: count }))170.sort((a, b) => b.count - a.count);171}172173export function getMachineInfo(): IMachineInfo {174175const machineInfo: IMachineInfo = {176os: `${osLib.type()} ${osLib.arch()} ${osLib.release()}`,177memory: `${(osLib.totalmem() / ByteSize.GB).toFixed(2)}GB (${(osLib.freemem() / ByteSize.GB).toFixed(2)}GB free)`,178vmHint: `${Math.round((virtualMachineHint.value() * 100))}%`,179};180181const cpus = osLib.cpus();182if (cpus && cpus.length > 0) {183machineInfo.cpus = `${cpus[0].model} (${cpus.length} x ${cpus[0].speed})`;184}185186return machineInfo;187}188189export async function collectLaunchConfigs(folder: string): Promise<WorkspaceStatItem[]> {190try {191const launchConfigs = new Map<string, number>();192const launchConfig = join(folder, '.vscode', 'launch.json');193194const contents = await fs.promises.readFile(launchConfig);195196const errors: ParseError[] = [];197const json = parse(contents.toString(), errors);198if (errors.length) {199console.log(`Unable to parse ${launchConfig}`);200return [];201}202203if (getNodeType(json) === 'object' && json['configurations']) {204for (const each of json['configurations']) {205const type = each['type'];206if (type) {207if (launchConfigs.has(type)) {208launchConfigs.set(type, launchConfigs.get(type)! + 1);209} else {210launchConfigs.set(type, 1);211}212}213}214}215216return asSortedItems(launchConfigs);217} catch (error) {218return [];219}220}221222export class DiagnosticsService implements IDiagnosticsService {223224declare readonly _serviceBrand: undefined;225226constructor(227@ITelemetryService private readonly telemetryService: ITelemetryService,228@IProductService private readonly productService: IProductService229) { }230231private formatMachineInfo(info: IMachineInfo): string {232const output: string[] = [];233output.push(`OS Version: ${info.os}`);234output.push(`CPUs: ${info.cpus}`);235output.push(`Memory (System): ${info.memory}`);236output.push(`VM: ${info.vmHint}`);237238return output.join('\n');239}240241private formatEnvironment(info: IMainProcessDiagnostics): string {242const output: string[] = [];243output.push(`Version: ${this.productService.nameShort} ${this.productService.version} (${this.productService.commit || 'Commit unknown'}, ${this.productService.date || 'Date unknown'})`);244output.push(`OS Version: ${osLib.type()} ${osLib.arch()} ${osLib.release()}`);245const cpus = osLib.cpus();246if (cpus && cpus.length > 0) {247output.push(`CPUs: ${cpus[0].model} (${cpus.length} x ${cpus[0].speed})`);248}249output.push(`Memory (System): ${(osLib.totalmem() / ByteSize.GB).toFixed(2)}GB (${(osLib.freemem() / ByteSize.GB).toFixed(2)}GB free)`);250if (!isWindows) {251output.push(`Load (avg): ${osLib.loadavg().map(l => Math.round(l)).join(', ')}`); // only provided on Linux/macOS252}253output.push(`VM: ${Math.round((virtualMachineHint.value() * 100))}%`);254output.push(`Screen Reader: ${info.screenReader ? 'yes' : 'no'}`);255output.push(`Process Argv: ${info.mainArguments.join(' ')}`);256output.push(`GPU Status: ${this.expandGPUFeatures(info.gpuFeatureStatus)}`);257258return output.join('\n');259}260261public async getPerformanceInfo(info: IMainProcessDiagnostics, remoteData: (IRemoteDiagnosticInfo | IRemoteDiagnosticError)[]): Promise<PerformanceInfo> {262return Promise.all([listProcesses(info.mainPID), this.formatWorkspaceMetadata(info)]).then(async result => {263let [rootProcess, workspaceInfo] = result;264let processInfo = this.formatProcessList(info, rootProcess);265266remoteData.forEach(diagnostics => {267if (isRemoteDiagnosticError(diagnostics)) {268processInfo += `\n${diagnostics.errorMessage}`;269workspaceInfo += `\n${diagnostics.errorMessage}`;270} else {271processInfo += `\n\nRemote: ${diagnostics.hostName}`;272if (diagnostics.processes) {273processInfo += `\n${this.formatProcessList(info, diagnostics.processes)}`;274}275276if (diagnostics.workspaceMetadata) {277workspaceInfo += `\n| Remote: ${diagnostics.hostName}`;278for (const folder of Object.keys(diagnostics.workspaceMetadata)) {279const metadata = diagnostics.workspaceMetadata[folder];280281let countMessage = `${metadata.fileCount} files`;282if (metadata.maxFilesReached) {283countMessage = `more than ${countMessage}`;284}285286workspaceInfo += `| Folder (${folder}): ${countMessage}`;287workspaceInfo += this.formatWorkspaceStats(metadata);288}289}290}291});292293return {294processInfo,295workspaceInfo296};297});298}299300public async getSystemInfo(info: IMainProcessDiagnostics, remoteData: (IRemoteDiagnosticInfo | IRemoteDiagnosticError)[]): Promise<SystemInfo> {301const { memory, vmHint, os, cpus } = getMachineInfo();302const systemInfo: SystemInfo = {303os,304memory,305cpus,306vmHint,307processArgs: `${info.mainArguments.join(' ')}`,308gpuStatus: info.gpuFeatureStatus,309screenReader: `${info.screenReader ? 'yes' : 'no'}`,310remoteData311};312313if (!isWindows) {314systemInfo.load = `${osLib.loadavg().map(l => Math.round(l)).join(', ')}`;315}316317if (isLinux) {318systemInfo.linuxEnv = {319desktopSession: process.env['DESKTOP_SESSION'],320xdgSessionDesktop: process.env['XDG_SESSION_DESKTOP'],321xdgCurrentDesktop: process.env['XDG_CURRENT_DESKTOP'],322xdgSessionType: process.env['XDG_SESSION_TYPE']323};324}325326return Promise.resolve(systemInfo);327}328329public async getDiagnostics(info: IMainProcessDiagnostics, remoteDiagnostics: (IRemoteDiagnosticInfo | IRemoteDiagnosticError)[]): Promise<string> {330const output: string[] = [];331return listProcesses(info.mainPID).then(async rootProcess => {332333// Environment Info334output.push('');335output.push(this.formatEnvironment(info));336337// Process List338output.push('');339output.push(this.formatProcessList(info, rootProcess));340341// Workspace Stats342if (info.windows.some(window => window.folderURIs && window.folderURIs.length > 0 && !window.remoteAuthority)) {343output.push('');344output.push('Workspace Stats: ');345output.push(await this.formatWorkspaceMetadata(info));346}347348remoteDiagnostics.forEach(diagnostics => {349if (isRemoteDiagnosticError(diagnostics)) {350output.push(`\n${diagnostics.errorMessage}`);351} else {352output.push('\n\n');353output.push(`Remote: ${diagnostics.hostName}`);354output.push(this.formatMachineInfo(diagnostics.machineInfo));355356if (diagnostics.processes) {357output.push(this.formatProcessList(info, diagnostics.processes));358}359360if (diagnostics.workspaceMetadata) {361for (const folder of Object.keys(diagnostics.workspaceMetadata)) {362const metadata = diagnostics.workspaceMetadata[folder];363364let countMessage = `${metadata.fileCount} files`;365if (metadata.maxFilesReached) {366countMessage = `more than ${countMessage}`;367}368369output.push(`Folder (${folder}): ${countMessage}`);370output.push(this.formatWorkspaceStats(metadata));371}372}373}374});375376output.push('');377output.push('');378379return output.join('\n');380});381}382383private formatWorkspaceStats(workspaceStats: WorkspaceStats): string {384const output: string[] = [];385const lineLength = 60;386let col = 0;387388const appendAndWrap = (name: string, count: number) => {389const item = ` ${name}(${count})`;390391if (col + item.length > lineLength) {392output.push(line);393line = '| ';394col = line.length;395}396else {397col += item.length;398}399line += item;400};401402// File Types403let line = '| File types:';404const maxShown = 10;405const max = workspaceStats.fileTypes.length > maxShown ? maxShown : workspaceStats.fileTypes.length;406for (let i = 0; i < max; i++) {407const item = workspaceStats.fileTypes[i];408appendAndWrap(item.name, item.count);409}410output.push(line);411412// Conf Files413if (workspaceStats.configFiles.length >= 0) {414line = '| Conf files:';415col = 0;416workspaceStats.configFiles.forEach((item) => {417appendAndWrap(item.name, item.count);418});419output.push(line);420}421422if (workspaceStats.launchConfigFiles.length > 0) {423let line = '| Launch Configs:';424workspaceStats.launchConfigFiles.forEach(each => {425const item = each.count > 1 ? ` ${each.name}(${each.count})` : ` ${each.name}`;426line += item;427});428output.push(line);429}430return output.join('\n');431}432433private expandGPUFeatures(gpuFeatures: any): string {434const longestFeatureName = Math.max(...Object.keys(gpuFeatures).map(feature => feature.length));435// Make columns aligned by adding spaces after feature name436return Object.keys(gpuFeatures).map(feature => `${feature}: ${' '.repeat(longestFeatureName - feature.length)} ${gpuFeatures[feature]}`).join('\n ');437}438439private formatWorkspaceMetadata(info: IMainProcessDiagnostics): Promise<string> {440const output: string[] = [];441const workspaceStatPromises: Promise<void>[] = [];442443info.windows.forEach(window => {444if (window.folderURIs.length === 0 || !!window.remoteAuthority) {445return;446}447448output.push(`| Window (${window.title})`);449450window.folderURIs.forEach(uriComponents => {451const folderUri = URI.revive(uriComponents);452if (folderUri.scheme === Schemas.file) {453const folder = folderUri.fsPath;454workspaceStatPromises.push(collectWorkspaceStats(folder, ['node_modules', '.git']).then(stats => {455let countMessage = `${stats.fileCount} files`;456if (stats.maxFilesReached) {457countMessage = `more than ${countMessage}`;458}459output.push(`| Folder (${basename(folder)}): ${countMessage}`);460output.push(this.formatWorkspaceStats(stats));461462}).catch(error => {463output.push(`| Error: Unable to collect workspace stats for folder ${folder} (${error.toString()})`);464}));465} else {466output.push(`| Folder (${folderUri.toString()}): Workspace stats not available.`);467}468});469});470471return Promise.all(workspaceStatPromises)472.then(_ => output.join('\n'))473.catch(e => `Unable to collect workspace stats: ${e}`);474}475476private formatProcessList(info: IMainProcessDiagnostics, rootProcess: ProcessItem): string {477const mapProcessToName = new Map<number, string>();478info.windows.forEach(window => mapProcessToName.set(window.pid, `window [${window.id}] (${window.title})`));479info.pidToNames.forEach(({ pid, name }) => mapProcessToName.set(pid, name));480481const output: string[] = [];482483output.push('CPU %\tMem MB\t PID\tProcess');484485if (rootProcess) {486this.formatProcessItem(info.mainPID, mapProcessToName, output, rootProcess, 0);487}488489return output.join('\n');490}491492private formatProcessItem(mainPid: number, mapProcessToName: Map<number, string>, output: string[], item: ProcessItem, indent: number): void {493const isRoot = (indent === 0);494495// Format name with indent496let name: string;497if (isRoot) {498name = item.pid === mainPid ? this.productService.applicationName : 'remote-server';499} else {500if (mapProcessToName.has(item.pid)) {501name = mapProcessToName.get(item.pid)!;502} else {503name = `${' '.repeat(indent)} ${item.name}`;504}505}506507const memory = process.platform === 'win32' ? item.mem : (osLib.totalmem() * (item.mem / 100));508output.push(`${item.load.toFixed(0).padStart(5, ' ')}\t${(memory / ByteSize.MB).toFixed(0).padStart(6, ' ')}\t${item.pid.toFixed(0).padStart(6, ' ')}\t${name}`);509510// Recurse into children if any511if (Array.isArray(item.children)) {512item.children.forEach(child => this.formatProcessItem(mainPid, mapProcessToName, output, child, indent + 1));513}514}515516public async getWorkspaceFileExtensions(workspace: IWorkspace): Promise<{ extensions: string[] }> {517const items = new Set<string>();518for (const { uri } of workspace.folders) {519const folderUri = URI.revive(uri);520if (folderUri.scheme !== Schemas.file) {521continue;522}523const folder = folderUri.fsPath;524try {525const stats = await collectWorkspaceStats(folder, ['node_modules', '.git']);526stats.fileTypes.forEach(item => items.add(item.name));527} catch { }528}529return { extensions: [...items] };530}531532public async reportWorkspaceStats(workspace: IWorkspaceInformation): Promise<void> {533for (const { uri } of workspace.folders) {534const folderUri = URI.revive(uri);535if (folderUri.scheme !== Schemas.file) {536continue;537}538539const folder = folderUri.fsPath;540try {541const stats = await collectWorkspaceStats(folder, ['node_modules', '.git']);542type WorkspaceStatsClassification = {543owner: 'lramos15';544comment: 'Metadata related to the workspace';545'workspace.id': { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'A UUID given to a workspace to identify it.' };546rendererSessionId: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The ID of the session' };547};548type WorkspaceStatsEvent = {549'workspace.id': string | undefined;550rendererSessionId: string;551};552this.telemetryService.publicLog2<WorkspaceStatsEvent, WorkspaceStatsClassification>('workspace.stats', {553'workspace.id': workspace.telemetryId,554rendererSessionId: workspace.rendererSessionId555});556type WorkspaceStatsFileClassification = {557owner: 'lramos15';558comment: 'Helps us gain insights into what type of files are being used in a workspace';559rendererSessionId: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The ID of the session.' };560type: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The type of file' };561count: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'How many types of that file are present' };562};563type WorkspaceStatsFileEvent = {564rendererSessionId: string;565type: string;566count: number;567};568stats.fileTypes.forEach(e => {569this.telemetryService.publicLog2<WorkspaceStatsFileEvent, WorkspaceStatsFileClassification>('workspace.stats.file', {570rendererSessionId: workspace.rendererSessionId,571type: e.name,572count: e.count573});574});575stats.launchConfigFiles.forEach(e => {576this.telemetryService.publicLog2<WorkspaceStatsFileEvent, WorkspaceStatsFileClassification>('workspace.stats.launchConfigFile', {577rendererSessionId: workspace.rendererSessionId,578type: e.name,579count: e.count580});581});582stats.configFiles.forEach(e => {583this.telemetryService.publicLog2<WorkspaceStatsFileEvent, WorkspaceStatsFileClassification>('workspace.stats.configFiles', {584rendererSessionId: workspace.rendererSessionId,585type: e.name,586count: e.count587});588});589590// Workspace stats metadata591type WorkspaceStatsMetadataClassification = {592owner: 'jrieken';593comment: 'Metadata about workspace metadata collection';594duration: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'How did it take to make workspace stats' };595reachedLimit: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'Did making workspace stats reach its limits' };596fileCount: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'How many files did workspace stats discover' };597readdirCount: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'How many readdir call were needed' };598};599type WorkspaceStatsMetadata = {600duration: number;601reachedLimit: boolean;602fileCount: number;603readdirCount: number;604};605this.telemetryService.publicLog2<WorkspaceStatsMetadata, WorkspaceStatsMetadataClassification>('workspace.stats.metadata', { duration: stats.totalScanTime, reachedLimit: stats.maxFilesReached, fileCount: stats.fileCount, readdirCount: stats.totalReaddirCount });606} catch {607// Report nothing if collecting metadata fails.608}609}610}611}612613614