Path: blob/main/src/vs/platform/diagnostics/node/diagnosticsService.ts
5220 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: 'claude-settings', filePattern: /^settings\.json$/i, relativePathPattern: /^\.claude$/i },69{ tag: 'claude-settings-local', filePattern: /^settings\.local\.json$/i, relativePathPattern: /^\.claude$/i },70{ tag: 'claude-mcp', filePattern: /^mcp\.json$/i, relativePathPattern: /^\.claude$/i },71{ tag: 'claude-commands-dir', filePattern: /\.md$/i, relativePathPattern: /^\.claude[\/\\]commands$/i },72{ tag: 'claude-skills-dir', filePattern: /^SKILL\.md$/i, relativePathPattern: /^\.claude[\/\\]skills[\/\\]/i },73{ tag: 'claude-rules-dir', filePattern: /\.md$/i, relativePathPattern: /^\.claude[\/\\]rules$/i },74{ tag: 'gemini.md', filePattern: /^gemini\.md$/i },75{ tag: 'copilot-instructions.md', filePattern: /^copilot\-instructions\.md$/i, relativePathPattern: /^\.github$/i },76];7778const fileTypes = new Map<string, number>();79const configFiles = new Map<string, number>();8081const MAX_FILES = 20000;8283function collect(root: string, dir: string, filter: string[], token: { count: number; maxReached: boolean; readdirCount: number }): Promise<void> {84const relativePath = dir.substring(root.length + 1);8586return Promises.withAsyncBody(async resolve => {87let files: IDirent[];8889token.readdirCount++;90try {91files = await pfs.readdir(dir, { withFileTypes: true });92} catch (error) {93// Ignore folders that can't be read94resolve();95return;96}9798if (token.count >= MAX_FILES) {99token.count += files.length;100token.maxReached = true;101resolve();102return;103}104105let pending = files.length;106if (pending === 0) {107resolve();108return;109}110111let filesToRead = files;112if (token.count + files.length > MAX_FILES) {113token.maxReached = true;114pending = MAX_FILES - token.count;115filesToRead = files.slice(0, pending);116}117118token.count += files.length;119120for (const file of filesToRead) {121if (file.isDirectory()) {122if (!filter.includes(file.name)) {123await collect(root, join(dir, file.name), filter, token);124}125126if (--pending === 0) {127resolve();128return;129}130} else {131const index = file.name.lastIndexOf('.');132if (index >= 0) {133const fileType = file.name.substring(index + 1);134if (fileType) {135fileTypes.set(fileType, (fileTypes.get(fileType) ?? 0) + 1);136}137}138139for (const configFile of configFilePatterns) {140if (configFile.relativePathPattern?.test(relativePath) !== false && configFile.filePattern.test(file.name)) {141configFiles.set(configFile.tag, (configFiles.get(configFile.tag) ?? 0) + 1);142}143}144145if (--pending === 0) {146resolve();147return;148}149}150}151});152}153154const statsPromise = Promises.withAsyncBody<WorkspaceStats>(async (resolve) => {155const token: { count: number; maxReached: boolean; readdirCount: number } = { count: 0, maxReached: false, readdirCount: 0 };156const sw = new StopWatch(true);157await collect(folder, folder, filter, token);158const launchConfigs = await collectLaunchConfigs(folder);159resolve({160configFiles: asSortedItems(configFiles),161fileTypes: asSortedItems(fileTypes),162fileCount: token.count,163maxFilesReached: token.maxReached,164launchConfigFiles: launchConfigs,165totalScanTime: sw.elapsed(),166totalReaddirCount: token.readdirCount167});168});169170workspaceStatsCache.set(cacheKey, statsPromise);171return statsPromise;172}173174function asSortedItems(items: Map<string, number>): WorkspaceStatItem[] {175return Array.from(items.entries(), ([name, count]) => ({ name: name, count: count }))176.sort((a, b) => b.count - a.count);177}178179export function getMachineInfo(): IMachineInfo {180181const machineInfo: IMachineInfo = {182os: `${osLib.type()} ${osLib.arch()} ${osLib.release()}`,183memory: `${(osLib.totalmem() / ByteSize.GB).toFixed(2)}GB (${(osLib.freemem() / ByteSize.GB).toFixed(2)}GB free)`,184vmHint: `${Math.round((virtualMachineHint.value() * 100))}%`,185};186187const cpus = osLib.cpus();188if (cpus && cpus.length > 0) {189machineInfo.cpus = `${cpus[0].model} (${cpus.length} x ${cpus[0].speed})`;190}191192return machineInfo;193}194195export async function collectLaunchConfigs(folder: string): Promise<WorkspaceStatItem[]> {196try {197const launchConfigs = new Map<string, number>();198const launchConfig = join(folder, '.vscode', 'launch.json');199200const contents = await fs.promises.readFile(launchConfig);201202const errors: ParseError[] = [];203const json = parse(contents.toString(), errors);204if (errors.length) {205console.log(`Unable to parse ${launchConfig}`);206return [];207}208209if (getNodeType(json) === 'object' && json['configurations']) {210for (const each of json['configurations']) {211const type = each['type'];212if (type) {213if (launchConfigs.has(type)) {214launchConfigs.set(type, launchConfigs.get(type)! + 1);215} else {216launchConfigs.set(type, 1);217}218}219}220}221222return asSortedItems(launchConfigs);223} catch (error) {224return [];225}226}227228export class DiagnosticsService implements IDiagnosticsService {229230declare readonly _serviceBrand: undefined;231232constructor(233@ITelemetryService private readonly telemetryService: ITelemetryService,234@IProductService private readonly productService: IProductService235) { }236237private formatMachineInfo(info: IMachineInfo): string {238const output: string[] = [];239output.push(`OS Version: ${info.os}`);240output.push(`CPUs: ${info.cpus}`);241output.push(`Memory (System): ${info.memory}`);242output.push(`VM: ${info.vmHint}`);243244return output.join('\n');245}246247private formatEnvironment(info: IMainProcessDiagnostics): string {248const output: string[] = [];249output.push(`Version: ${this.productService.nameShort} ${this.productService.version} (${this.productService.commit || 'Commit unknown'}, ${this.productService.date || 'Date unknown'})`);250output.push(`OS Version: ${osLib.type()} ${osLib.arch()} ${osLib.release()}`);251const cpus = osLib.cpus();252if (cpus && cpus.length > 0) {253output.push(`CPUs: ${cpus[0].model} (${cpus.length} x ${cpus[0].speed})`);254}255output.push(`Memory (System): ${(osLib.totalmem() / ByteSize.GB).toFixed(2)}GB (${(osLib.freemem() / ByteSize.GB).toFixed(2)}GB free)`);256if (!isWindows) {257output.push(`Load (avg): ${osLib.loadavg().map(l => Math.round(l)).join(', ')}`); // only provided on Linux/macOS258}259output.push(`VM: ${Math.round((virtualMachineHint.value() * 100))}%`);260output.push(`Screen Reader: ${info.screenReader ? 'yes' : 'no'}`);261output.push(`Process Argv: ${info.mainArguments.join(' ')}`);262output.push(`GPU Status: ${this.expandGPUFeatures(info.gpuFeatureStatus)}`);263if (info.gpuLogMessages && info.gpuLogMessages.length > 0) {264output.push(`GPU Log Messages:`);265info.gpuLogMessages.forEach(msg => {266output.push(`${msg.header}: ${msg.message}`);267});268}269270return output.join('\n');271}272273public async getPerformanceInfo(info: IMainProcessDiagnostics, remoteData: (IRemoteDiagnosticInfo | IRemoteDiagnosticError)[]): Promise<PerformanceInfo> {274return Promise.all([listProcesses(info.mainPID), this.formatWorkspaceMetadata(info)]).then(async result => {275let [rootProcess, workspaceInfo] = result;276let processInfo = this.formatProcessList(info, rootProcess);277278remoteData.forEach(diagnostics => {279if (isRemoteDiagnosticError(diagnostics)) {280processInfo += `\n${diagnostics.errorMessage}`;281workspaceInfo += `\n${diagnostics.errorMessage}`;282} else {283processInfo += `\n\nRemote: ${diagnostics.hostName}`;284if (diagnostics.processes) {285processInfo += `\n${this.formatProcessList(info, diagnostics.processes)}`;286}287288if (diagnostics.workspaceMetadata) {289workspaceInfo += `\n| Remote: ${diagnostics.hostName}`;290for (const folder of Object.keys(diagnostics.workspaceMetadata)) {291const metadata = diagnostics.workspaceMetadata[folder];292293let countMessage = `${metadata.fileCount} files`;294if (metadata.maxFilesReached) {295countMessage = `more than ${countMessage}`;296}297298workspaceInfo += `| Folder (${folder}): ${countMessage}`;299workspaceInfo += this.formatWorkspaceStats(metadata);300}301}302}303});304305return {306processInfo,307workspaceInfo308};309});310}311312public async getSystemInfo(info: IMainProcessDiagnostics, remoteData: (IRemoteDiagnosticInfo | IRemoteDiagnosticError)[]): Promise<SystemInfo> {313const { memory, vmHint, os, cpus } = getMachineInfo();314const systemInfo: SystemInfo = {315os,316memory,317cpus,318vmHint,319processArgs: `${info.mainArguments.join(' ')}`,320gpuStatus: info.gpuFeatureStatus,321screenReader: `${info.screenReader ? 'yes' : 'no'}`,322remoteData323};324325if (!isWindows) {326systemInfo.load = `${osLib.loadavg().map(l => Math.round(l)).join(', ')}`;327}328329if (isLinux) {330systemInfo.linuxEnv = {331desktopSession: process.env['DESKTOP_SESSION'],332xdgSessionDesktop: process.env['XDG_SESSION_DESKTOP'],333xdgCurrentDesktop: process.env['XDG_CURRENT_DESKTOP'],334xdgSessionType: process.env['XDG_SESSION_TYPE']335};336}337338return Promise.resolve(systemInfo);339}340341public async getDiagnostics(info: IMainProcessDiagnostics, remoteDiagnostics: (IRemoteDiagnosticInfo | IRemoteDiagnosticError)[]): Promise<string> {342const output: string[] = [];343return listProcesses(info.mainPID).then(async rootProcess => {344345// Environment Info346output.push('');347output.push(this.formatEnvironment(info));348349// Process List350output.push('');351output.push(this.formatProcessList(info, rootProcess));352353// Workspace Stats354if (info.windows.some(window => window.folderURIs && window.folderURIs.length > 0 && !window.remoteAuthority)) {355output.push('');356output.push('Workspace Stats: ');357output.push(await this.formatWorkspaceMetadata(info));358}359360remoteDiagnostics.forEach(diagnostics => {361if (isRemoteDiagnosticError(diagnostics)) {362output.push(`\n${diagnostics.errorMessage}`);363} else {364output.push('\n\n');365output.push(`Remote: ${diagnostics.hostName}`);366output.push(this.formatMachineInfo(diagnostics.machineInfo));367368if (diagnostics.processes) {369output.push(this.formatProcessList(info, diagnostics.processes));370}371372if (diagnostics.workspaceMetadata) {373for (const folder of Object.keys(diagnostics.workspaceMetadata)) {374const metadata = diagnostics.workspaceMetadata[folder];375376let countMessage = `${metadata.fileCount} files`;377if (metadata.maxFilesReached) {378countMessage = `more than ${countMessage}`;379}380381output.push(`Folder (${folder}): ${countMessage}`);382output.push(this.formatWorkspaceStats(metadata));383}384}385}386});387388output.push('');389output.push('');390391return output.join('\n');392});393}394395private formatWorkspaceStats(workspaceStats: WorkspaceStats): string {396const output: string[] = [];397const lineLength = 60;398let col = 0;399400const appendAndWrap = (name: string, count: number) => {401const item = ` ${name}(${count})`;402403if (col + item.length > lineLength) {404output.push(line);405line = '| ';406col = line.length;407}408else {409col += item.length;410}411line += item;412};413414// File Types415let line = '| File types:';416const maxShown = 10;417const max = workspaceStats.fileTypes.length > maxShown ? maxShown : workspaceStats.fileTypes.length;418for (let i = 0; i < max; i++) {419const item = workspaceStats.fileTypes[i];420appendAndWrap(item.name, item.count);421}422output.push(line);423424// Conf Files425if (workspaceStats.configFiles.length >= 0) {426line = '| Conf files:';427col = 0;428workspaceStats.configFiles.forEach((item) => {429appendAndWrap(item.name, item.count);430});431output.push(line);432}433434if (workspaceStats.launchConfigFiles.length > 0) {435let line = '| Launch Configs:';436workspaceStats.launchConfigFiles.forEach(each => {437const item = each.count > 1 ? ` ${each.name}(${each.count})` : ` ${each.name}`;438line += item;439});440output.push(line);441}442return output.join('\n');443}444445private expandGPUFeatures(gpuFeatures: Record<string, string>): string {446const longestFeatureName = Math.max(...Object.keys(gpuFeatures).map(feature => feature.length));447// Make columns aligned by adding spaces after feature name448return Object.keys(gpuFeatures).map(feature => `${feature}: ${' '.repeat(longestFeatureName - feature.length)} ${gpuFeatures[feature]}`).join('\n ');449}450451private formatWorkspaceMetadata(info: IMainProcessDiagnostics): Promise<string> {452const output: string[] = [];453const workspaceStatPromises: Promise<void>[] = [];454455info.windows.forEach(window => {456if (window.folderURIs.length === 0 || !!window.remoteAuthority) {457return;458}459460output.push(`| Window (${window.title})`);461462window.folderURIs.forEach(uriComponents => {463const folderUri = URI.revive(uriComponents);464if (folderUri.scheme === Schemas.file) {465const folder = folderUri.fsPath;466workspaceStatPromises.push(collectWorkspaceStats(folder, ['node_modules', '.git']).then(stats => {467let countMessage = `${stats.fileCount} files`;468if (stats.maxFilesReached) {469countMessage = `more than ${countMessage}`;470}471output.push(`| Folder (${basename(folder)}): ${countMessage}`);472output.push(this.formatWorkspaceStats(stats));473474}).catch(error => {475output.push(`| Error: Unable to collect workspace stats for folder ${folder} (${error.toString()})`);476}));477} else {478output.push(`| Folder (${folderUri.toString()}): Workspace stats not available.`);479}480});481});482483return Promise.all(workspaceStatPromises)484.then(_ => output.join('\n'))485.catch(e => `Unable to collect workspace stats: ${e}`);486}487488private formatProcessList(info: IMainProcessDiagnostics, rootProcess: ProcessItem): string {489const mapProcessToName = new Map<number, string>();490info.windows.forEach(window => mapProcessToName.set(window.pid, `window [${window.id}] (${window.title})`));491info.pidToNames.forEach(({ pid, name }) => mapProcessToName.set(pid, name));492493const output: string[] = [];494495output.push('CPU %\tMem MB\t PID\tProcess');496497if (rootProcess) {498this.formatProcessItem(info.mainPID, mapProcessToName, output, rootProcess, 0);499}500501return output.join('\n');502}503504private formatProcessItem(mainPid: number, mapProcessToName: Map<number, string>, output: string[], item: ProcessItem, indent: number): void {505const isRoot = (indent === 0);506507// Format name with indent508let name: string;509if (isRoot) {510name = item.pid === mainPid ? this.productService.applicationName : 'remote-server';511} else {512if (mapProcessToName.has(item.pid)) {513name = mapProcessToName.get(item.pid)!;514} else {515name = `${' '.repeat(indent)} ${item.name}`;516}517}518519const memory = process.platform === 'win32' ? item.mem : (osLib.totalmem() * (item.mem / 100));520output.push(`${item.load.toFixed(0).padStart(5, ' ')}\t${(memory / ByteSize.MB).toFixed(0).padStart(6, ' ')}\t${item.pid.toFixed(0).padStart(6, ' ')}\t${name}`);521522// Recurse into children if any523if (Array.isArray(item.children)) {524item.children.forEach(child => this.formatProcessItem(mainPid, mapProcessToName, output, child, indent + 1));525}526}527528public async getWorkspaceFileExtensions(workspace: IWorkspace): Promise<{ extensions: string[] }> {529const items = new Set<string>();530for (const { uri } of workspace.folders) {531const folderUri = URI.revive(uri);532if (folderUri.scheme !== Schemas.file) {533continue;534}535const folder = folderUri.fsPath;536try {537const stats = await collectWorkspaceStats(folder, ['node_modules', '.git']);538stats.fileTypes.forEach(item => items.add(item.name));539} catch { }540}541return { extensions: [...items] };542}543544public async reportWorkspaceStats(workspace: IWorkspaceInformation): Promise<void> {545for (const { uri } of workspace.folders) {546const folderUri = URI.revive(uri);547if (folderUri.scheme !== Schemas.file) {548continue;549}550551const folder = folderUri.fsPath;552try {553const stats = await collectWorkspaceStats(folder, ['node_modules', '.git']);554type WorkspaceStatsClassification = {555owner: 'lramos15';556comment: 'Metadata related to the workspace';557'workspace.id': { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'A UUID given to a workspace to identify it.' };558rendererSessionId: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The ID of the session' };559};560type WorkspaceStatsEvent = {561'workspace.id': string | undefined;562rendererSessionId: string;563};564this.telemetryService.publicLog2<WorkspaceStatsEvent, WorkspaceStatsClassification>('workspace.stats', {565'workspace.id': workspace.telemetryId,566rendererSessionId: workspace.rendererSessionId567});568type WorkspaceStatsFileClassification = {569owner: 'lramos15';570comment: 'Helps us gain insights into what type of files are being used in a workspace';571rendererSessionId: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The ID of the session.' };572type: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The type of file' };573count: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'How many types of that file are present' };574};575type WorkspaceStatsFileEvent = {576rendererSessionId: string;577type: string;578count: number;579};580stats.fileTypes.forEach(e => {581this.telemetryService.publicLog2<WorkspaceStatsFileEvent, WorkspaceStatsFileClassification>('workspace.stats.file', {582rendererSessionId: workspace.rendererSessionId,583type: e.name,584count: e.count585});586});587stats.launchConfigFiles.forEach(e => {588this.telemetryService.publicLog2<WorkspaceStatsFileEvent, WorkspaceStatsFileClassification>('workspace.stats.launchConfigFile', {589rendererSessionId: workspace.rendererSessionId,590type: e.name,591count: e.count592});593});594stats.configFiles.forEach(e => {595this.telemetryService.publicLog2<WorkspaceStatsFileEvent, WorkspaceStatsFileClassification>('workspace.stats.configFiles', {596rendererSessionId: workspace.rendererSessionId,597type: e.name,598count: e.count599});600});601602// Workspace stats metadata603type WorkspaceStatsMetadataClassification = {604owner: 'jrieken';605comment: 'Metadata about workspace metadata collection';606duration: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'How did it take to make workspace stats' };607reachedLimit: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'Did making workspace stats reach its limits' };608fileCount: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'How many files did workspace stats discover' };609readdirCount: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'How many readdir call were needed' };610};611type WorkspaceStatsMetadata = {612duration: number;613reachedLimit: boolean;614fileCount: number;615readdirCount: number;616};617this.telemetryService.publicLog2<WorkspaceStatsMetadata, WorkspaceStatsMetadataClassification>('workspace.stats.metadata', { duration: stats.totalScanTime, reachedLimit: stats.maxFilesReached, fileCount: stats.fileCount, readdirCount: stats.totalReaddirCount });618} catch {619// Report nothing if collecting metadata fails.620}621}622}623}624625626