diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index a0622f4b4..08cd130da 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -200,6 +200,11 @@ jobs: run: cd clients/vscode-hlasmplugin && npm ci && npx semantic-release --dry-run env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + - name: Inject telemetry key + run: | + cd clients/vscode-hlasmplugin/src + export TEL_KEY=`node -e "console.log(Buffer.from('${{ secrets.TELEMETRY_KEY }}').toString('base64'))"` + sed -i "s/const TELEMETRY_KEY_ENCODED = TELEMETRY_DEFAULT_KEY/const TELEMETRY_KEY_ENCODED = '$TEL_KEY'/" telemetry.ts - name: Package VSIX run: cd clients/vscode-hlasmplugin/ && npm ci && npm run package - name: Get VSIX filename @@ -231,6 +236,11 @@ jobs: run: cd clients/vscode-hlasmplugin && npm ci && npx semantic-release --dry-run env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + - name: Inject telemetry key + run: | + cd clients/vscode-hlasmplugin/src + export TEL_KEY=`node -e "console.log(Buffer.from('${{ secrets.TELEMETRY_KEY }}').toString('base64'))"` + sed -i "s/const TELEMETRY_KEY_ENCODED = TELEMETRY_DEFAULT_KEY/const TELEMETRY_KEY_ENCODED = '$TEL_KEY'/" telemetry.ts - name: Configure run: mkdir build && cd build && cmake -G Ninja ../ - name: Build diff --git a/benchmark/CMakeLists.txt b/benchmark/CMakeLists.txt index 6ba5dde1f..f00b81b94 100644 --- a/benchmark/CMakeLists.txt +++ b/benchmark/CMakeLists.txt @@ -12,7 +12,9 @@ project(benchmark) -add_executable(benchmark ${PROJECT_SOURCE_DIR}/benchmark.cpp) +add_executable(benchmark + benchmark.cpp + diagnostic_counter.h) target_link_libraries(benchmark nlohmann_json::nlohmann_json) diff --git a/benchmark/benchmark.cpp b/benchmark/benchmark.cpp index 620008dd6..cd2719242 100644 --- a/benchmark/benchmark.cpp +++ b/benchmark/benchmark.cpp @@ -22,7 +22,10 @@ #include #include +#include "../language_server/src/parsing_metadata_collector.h" +#include "../language_server/src/parsing_metadata_serialization.h" #include "config/pgm_conf.h" +#include "diagnostic_counter.h" #include "nlohmann/json.hpp" #include "workspace_manager.h" #include "workspaces/file_impl.h" @@ -59,47 +62,11 @@ * - Files - total number of parsed files */ -using json = nlohmann::json; - -class diagnostic_counter : public hlasm_plugin::parser_library::diagnostics_consumer -{ -public: - void consume_diagnostics(hlasm_plugin::parser_library::diagnostic_list diagnostics) override - { - for (size_t i = 0; i < diagnostics.diagnostics_size(); i++) - { - auto diag_sev = diagnostics.diagnostics(i).severity(); - if (diag_sev == hlasm_plugin::parser_library::diagnostic_severity::error) - error_count++; - else if (diag_sev == hlasm_plugin::parser_library::diagnostic_severity::warning) - warning_count++; - message_counts[diagnostics.diagnostics(i).code()]++; - } - } +using namespace hlasm_plugin; - void clear_counters() - { - error_count = 0; - warning_count = 0; - message_counts.clear(); - } - - size_t error_count = 0; - size_t warning_count = 0; - - std::unordered_map message_counts; -}; - -class metrics_collector : public hlasm_plugin::parser_library::performance_metrics_consumer -{ -public: - void consume_performance_metrics(const hlasm_plugin::parser_library::performance_metrics& metrics) override - { - metrics_ = metrics; - } +using json = nlohmann::json; - hlasm_plugin::parser_library::performance_metrics metrics_; -}; +namespace { struct all_file_stats { @@ -113,21 +80,6 @@ struct all_file_stats size_t failed_file_opens = 0; }; -json get_top_messages(const std::unordered_map& msgs, size_t limit = 3) -{ - std::vector> top_msgs(limit); - - constexpr const auto cmp_msg = [](const auto& a, const auto& b) { return a.second > b.second; }; - - const auto last = std::partial_sort_copy(msgs.begin(), msgs.end(), top_msgs.begin(), top_msgs.end(), cmp_msg); - top_msgs.erase(last, top_msgs.end()); - - json result = json::object(); - for (auto&& [key, value] : top_msgs) - result[std::move(key)] = value; - return result; -} - json parse_one_file(const std::string& source_file, const std::string& ws_folder, all_file_stats& s, @@ -150,10 +102,10 @@ json parse_one_file(const std::string& source_file, // new workspace manager hlasm_plugin::parser_library::workspace_manager ws; - diagnostic_counter diag_counter; + hlasm_plugin::benchmark::diagnostic_counter diag_counter; ws.register_diagnostics_consumer(&diag_counter); - metrics_collector collector; - ws.register_performance_metrics_consumer(&collector); + hlasm_plugin::language_server::parsing_metadata_collector collector; + ws.register_parsing_metadata_consumer(&collector); // input folder as new workspace ws.add_workspace(ws_folder.c_str(), ws_folder.c_str()); @@ -179,18 +131,19 @@ json parse_one_file(const std::string& source_file, return json({ { "File", source_file }, { "Success", false }, { "Reason", "Crash" } }); } + const auto& metrics = collector.data.metrics; + auto c_end = std::clock(); auto end = std::chrono::high_resolution_clock::now(); auto time = std::chrono::duration_cast(end - start).count(); - auto exec_statements = collector.metrics_.open_code_statements + collector.metrics_.copy_statements - + collector.metrics_.macro_statements + collector.metrics_.lookahead_statements - + collector.metrics_.reparsed_statements; + auto exec_statements = metrics.open_code_statements + metrics.copy_statements + metrics.macro_statements + + metrics.lookahead_statements + metrics.reparsed_statements; s.average_stmt_ms += (exec_statements / (double)time); - s.average_line_ms += collector.metrics_.lines / (double)time; - s.all_files += collector.metrics_.files; + s.average_line_ms += metrics.lines / (double)time; + s.all_files += metrics.files; s.whole_time += time; - auto top_messages = get_top_messages(diag_counter.message_counts); + auto top_messages = hlasm_plugin::benchmark::get_top_messages(diag_counter.message_counts); json result({ { "File", source_file }, { "Success", true }, @@ -198,23 +151,16 @@ json parse_one_file(const std::string& source_file, { "Warnings", diag_counter.warning_count }, { "Wall Time (ms)", time }, { "CPU Time (ms/n)", 1000.0 * (c_end - c_start) / CLOCKS_PER_SEC }, - { "Open Code Statements", collector.metrics_.open_code_statements }, - { "Copy Statements", collector.metrics_.copy_statements }, - { "Macro Statements", collector.metrics_.macro_statements }, - { "Copy Def Statements", collector.metrics_.copy_def_statements }, - { "Macro Def Statements", collector.metrics_.macro_def_statements }, - { "Lookahead Statements", collector.metrics_.lookahead_statements }, - { "Reparsed Statements", collector.metrics_.reparsed_statements }, - { "Continued Statements", collector.metrics_.continued_statements }, - { "Non-continued Statements", collector.metrics_.non_continued_statements }, { "Executed Statements", exec_statements }, - { "Lines", collector.metrics_.lines }, { "ExecStatement/ms", exec_statements / (double)time }, - { "Line/ms", collector.metrics_.lines / (double)time }, - { "Files", collector.metrics_.files }, + { "Line/ms", metrics.lines / (double)time }, { "Top messages", std::move(top_messages) } }); - auto first_parse_metrics = collector.metrics_; + json metrics_json(metrics); + + result.insert(metrics_json.begin(), metrics_json.end()); + + auto first_parse_metrics = metrics; auto first_diag_counter = diag_counter; long long reparse_time = 0; // Reparse to benchmark macro caching @@ -286,6 +232,7 @@ std::string get_file_message(size_t iter, size_t begin, size_t end, const std::s s << "[" << base_message << " " << iter << "/(" << begin << "-" << end << ")] "; return s.str(); } +} // namespace int main(int argc, char** argv) { @@ -449,4 +396,4 @@ int main(int argc, char** argv) std::clog << "Parse finished\n\n" << std::endl; } return 0; -} +} \ No newline at end of file diff --git a/benchmark/diagnostic_counter.h b/benchmark/diagnostic_counter.h new file mode 100644 index 000000000..be989552d --- /dev/null +++ b/benchmark/diagnostic_counter.h @@ -0,0 +1,72 @@ +/* + * Copyright (c) 2021 Broadcom. + * The term "Broadcom" refers to Broadcom Inc. and/or its subsidiaries. + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Broadcom, Inc. - initial API and implementation + */ + +#ifndef HLASMPLUGIN_LANGUAGESERVER_DIAGNOSTIC_COUNTER_H +#define HLASMPLUGIN_LANGUAGESERVER_DIAGNOSTIC_COUNTER_H + +#include "nlohmann/json.hpp" +#include "workspace_manager.h" + +namespace hlasm_plugin::benchmark { + +class diagnostic_counter : public hlasm_plugin::parser_library::diagnostics_consumer +{ +public: + void consume_diagnostics(hlasm_plugin::parser_library::diagnostic_list diagnostics) override + { + for (size_t i = 0; i < diagnostics.diagnostics_size(); i++) + { + if (auto diag_sev = diagnostics.diagnostics(i).severity(); + diag_sev == hlasm_plugin::parser_library::diagnostic_severity::error) + error_count++; + else if (diag_sev == hlasm_plugin::parser_library::diagnostic_severity::warning) + warning_count++; + message_counts[diagnostics.diagnostics(i).code()]++; + } + } + + void clear_counters() + { + error_count = 0; + warning_count = 0; + message_counts.clear(); + } + + size_t error_count = 0; + size_t warning_count = 0; + + std::unordered_map message_counts; +}; + +inline nlohmann::json get_top_messages(const std::unordered_map& msgs, size_t limit = 3) +{ + std::vector> top_msgs(limit); + + constexpr const auto cmp_msg = [](const auto& a, const auto& b) { return a.second > b.second; }; + + const auto last = std::partial_sort_copy(msgs.begin(), msgs.end(), top_msgs.begin(), top_msgs.end(), cmp_msg); + top_msgs.erase(last, top_msgs.end()); + + nlohmann::json result = nlohmann::json::object(); + for (auto&& [key, value] : top_msgs) + result[std::move(key)] = value; + return result; +} + + +} // namespace hlasm_plugin::benchmark + + + +#endif \ No newline at end of file diff --git a/clients/vscode-hlasmplugin/README.md b/clients/vscode-hlasmplugin/README.md index d1d68501c..280522a56 100644 --- a/clients/vscode-hlasmplugin/README.md +++ b/clients/vscode-hlasmplugin/README.md @@ -244,3 +244,30 @@ It can by configured using the `preprocessor` key in processor group: - If you have a question about how to accomplish something with the extension, or come across a problem, file an issue on [GitHub](https://github.com/eclipse/che-che4z-lsp-for-hlasm) - Contributions are always welcome! Please see our [GitHub](https://github.com/eclipse/che-che4z-lsp-for-hlasm) repository for more information. - Any and all feedback is appreciated and welcome! + +## Privacy Notice +The extensions for Visual Studio Code developed by Broadcom Inc., including its corporate affiliates and subsidiaries, ("Broadcom") are provided free of charge, but in order to better understand and meet its users’ needs, Broadcom may collect, use, analyze and retain anonymous users’ metadata and interaction data, (collectively, “Usage Data”) and aggregate such Usage Data with similar Usage Data of other Broadcom customers. Please find more detailed information in [License and Service Terms & Repository](https://www.broadcom.com/company/legal/licensing). + +This data collection uses built-in Microsoft VS Code Telemetry, which can be disabled, at your sole discretion, if you do not want to send Usage Data. See the `telemetry.telemetryLevel` and `telemetry.enableTelemetry` (deprecated) settings of VS Code. + +The current release of HLASM Language Support collects anonymous data for the following events: +* Activation of this VS Code extension +* Open and close of files +* Invocation of the following features: Autocomplete, Go to definition, Find all references and Hover +* Launch of Macro tracer and subsequent user interactions with the debugging interface (step into, step over, continue, set breakpoints) +* Malfunctions of the extension + +Each such event is logged with the following information: +* Event time +* Operating system and version +* Country or region +* Anonymous user and session ID +* Version numbers of Microsoft VS Code and HLASM Language Support +* Extension response time + +Additionally, when a file is opened, the following information are logged: +* Number of diagnostics (errors and warnings) +* Number of files that were parsed including macros and COPY files +* Number of parsed lines +* Whether the diagnostics were suppressed +* Whether matching processor group was found in the configuration diff --git a/clients/vscode-hlasmplugin/package-lock.json b/clients/vscode-hlasmplugin/package-lock.json index 1ad935c19..94495e067 100644 --- a/clients/vscode-hlasmplugin/package-lock.json +++ b/clients/vscode-hlasmplugin/package-lock.json @@ -5354,9 +5354,9 @@ } }, "nth-check": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.0.0.tgz", - "integrity": "sha512-i4sc/Kj8htBrAiH1viZ0TgU8Y5XqCaV/FziYK6TBczxmeKm3AEFWqqF3195yKudrarqy7Zu80Ra5dobFjn9X/Q==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.0.1.tgz", + "integrity": "sha512-it1vE95zF6dTT9lBsYbxvqh0Soy4SPowchj0UBGj/V6cTPnXXtQOPUbhZ6CmGzAD/rW22LQK6E96pcdJXk4A4w==", "dev": true, "requires": { "boolbase": "^1.0.0" @@ -5398,8 +5398,7 @@ "dependencies": { "ansi-regex": { "version": "5.0.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", - "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==", + "resolved": "", "dev": true }, "ansi-styles": { @@ -6292,8 +6291,7 @@ "dependencies": { "ansi-regex": { "version": "5.0.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", - "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==", + "resolved": "", "dev": true }, "ansi-styles": { @@ -6508,9 +6506,9 @@ } }, "semver-regex": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/semver-regex/-/semver-regex-3.1.2.tgz", - "integrity": "sha512-bXWyL6EAKOJa81XG1OZ/Yyuq+oT0b2YLlxx7c+mrdYPaPbnj6WgVULXhinMIeZGufuUBu/eVRqXEhiv4imfwxA==", + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/semver-regex/-/semver-regex-3.1.3.tgz", + "integrity": "sha512-Aqi54Mk9uYTjVexLnR67rTyBusmwd04cLkHy9hNvk3+G3nT2Oyg7E0l4XVbOaNwIvQ3hHeYxGcyEy+mKreyBFQ==", "dev": true }, "set-blocking": { @@ -7155,6 +7153,11 @@ "yazl": "^2.2.2" } }, + "vscode-extension-telemetry": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/vscode-extension-telemetry/-/vscode-extension-telemetry-0.4.2.tgz", + "integrity": "sha512-y0f51mVoFxHIzULQNCC26TBFIKdEC7uckS3tFoK++OOOl8mU2LlOxgmbd52T/SXoXNg5aI7xqs+4V2ug5ITvKw==" + }, "vscode-jsonrpc": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-4.0.0.tgz", diff --git a/clients/vscode-hlasmplugin/package.json b/clients/vscode-hlasmplugin/package.json index 25dde1d5a..9c0a14c10 100644 --- a/clients/vscode-hlasmplugin/package.json +++ b/clients/vscode-hlasmplugin/package.json @@ -59,6 +59,7 @@ "vscode-test": "^1.3.0" }, "dependencies": { + "vscode-extension-telemetry": "^0.4.2", "vscode-languageclient": "^5.2.1" }, "scripts": { diff --git a/clients/vscode-hlasmplugin/src/extension.ts b/clients/vscode-hlasmplugin/src/extension.ts index fef4ff3d3..811da70d4 100644 --- a/clients/vscode-hlasmplugin/src/extension.ts +++ b/clients/vscode-hlasmplugin/src/extension.ts @@ -23,6 +23,8 @@ import { CustomEditorCommands } from './customEditorCommands'; import { EventsHandler, getConfig } from './eventsHandler'; import { ServerFactory, ServerVariant } from './serverFactory'; import { HLASMDebugAdapterFactory } from './hlasmDebugAdapterFactory'; +import { Telemetry } from './telemetry'; +import { LanguageClientErrorHandler } from './languageClientErrorHandler'; const offset = 71; const continueColumn = 15; @@ -30,15 +32,37 @@ const continueColumn = 15; const sleep = (ms: number) => { return new Promise((resolve) => { setTimeout(resolve, ms) }); }; -//export var hlasmpluginClient : vscodelc.LanguageClient; + +function objectToString(o : any) { + if(o === null) + return null; + + Object.keys(o).forEach(k => { + o[k] = '' + o[k]; + }); + + return o; + } + /** * ACTIVATION * activates the extension */ export async function activate(context: vscode.ExtensionContext) { + const serverVariant = getConfig('serverVariant', 'native'); + + var telemetry = new Telemetry(); + context.subscriptions.push(telemetry); + + // setTimeout is needed, because telemetry initialization is asynchronous + // and AFAIK no event in the API is exposed to send the activation telemetry event + setTimeout(() => {telemetry.reportEvent("hlasm.activated", {server_variant:serverVariant.toString()});}, 1000); + // patterns for files and configs const filePattern: string = '**/*'; - + + const clientErrorHandler = new LanguageClientErrorHandler(telemetry); + // create client options const syncFileEvents = getConfig('syncFileEvents', true); const clientOptions: vscodelc.LanguageClientOptions = { @@ -50,16 +74,23 @@ export async function activate(context: vscode.ExtensionContext) { code2Protocol: (value: vscode.Uri) => value.toString(), protocol2Code: (value: string) => vscode.Uri.file((vscode.Uri.parse(value).fsPath)) - } + }, + errorHandler: clientErrorHandler }; + // create server options var factory = new ServerFactory(); - const serverVariant = getConfig('serverVariant', 'native'); + const serverOptions = await factory.create(serverVariant); //client init var hlasmpluginClient = new vscodelc.LanguageClient('Hlasmplugin Language Server', serverOptions, clientOptions); + + clientErrorHandler.defaultHandler = hlasmpluginClient.createDefaultErrorHandler(); + // The objectToString is necessary, because telemetry reporter only takes objects with + // string properties and there are some boolean that we receive from the language server + hlasmpluginClient.onTelemetry((object) => {telemetry.reportEvent(object.method_name, objectToString(object.properties), object.measurements)}); //asm contribution var highlight = new SemanticTokensFeature(hlasmpluginClient); @@ -80,6 +111,9 @@ export async function activate(context: vscode.ExtensionContext) { let api = { getExtension(): vscodelc.LanguageClient { return hlasmpluginClient; + }, + getTelemetry(): Telemetry { + return telemetry; } }; return api; diff --git a/clients/vscode-hlasmplugin/src/languageClientErrorHandler.ts b/clients/vscode-hlasmplugin/src/languageClientErrorHandler.ts new file mode 100644 index 000000000..4d6d115b2 --- /dev/null +++ b/clients/vscode-hlasmplugin/src/languageClientErrorHandler.ts @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2021 Broadcom. + * The term "Broadcom" refers to Broadcom Inc. and/or its subsidiaries. + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Broadcom, Inc. - initial API and implementation + */ + +import * as vscodelc from 'vscode-languageclient'; + +import { ErrorHandler } from 'vscode-languageclient'; +import { Telemetry } from './telemetry'; + +export class LanguageClientErrorHandler implements ErrorHandler +{ + defaultHandler : ErrorHandler = undefined; + telemetry : Telemetry; + + constructor(tlmtry : Telemetry) + { + this.telemetry = tlmtry; + } + + error(error: Error, message: vscodelc.Message, count: number): vscodelc.ErrorAction { + this.telemetry.reportEvent("hlasm.connectionError", error) + + return this.defaultHandler.error(error, message, count); + } + closed(): vscodelc.CloseAction { + this.telemetry.reportEvent("hlasm.connectionClosed") + return this.defaultHandler.closed(); + } + +} \ No newline at end of file diff --git a/clients/vscode-hlasmplugin/src/telemetry.ts b/clients/vscode-hlasmplugin/src/telemetry.ts new file mode 100644 index 000000000..bd51cf6e0 --- /dev/null +++ b/clients/vscode-hlasmplugin/src/telemetry.ts @@ -0,0 +1,56 @@ +/* + * Copyright (c) 2020 Broadcom. + * The term "Broadcom" refers to Broadcom Inc. and/or its subsidiaries. + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Broadcom, Inc. - initial API and implementation + */ +import * as vscode from "vscode"; + +import TelemetryReporter from 'vscode-extension-telemetry'; + +const EXTENSION_ID = "broadcommfd.hlasm-language-support"; +const TELEMETRY_DEFAULT_KEY = "NOT_TELEMETRY_KEY"; + +// The following line is replaced by base64 encoded telemetry key in the CI +const TELEMETRY_KEY_ENCODED = TELEMETRY_DEFAULT_KEY; + +export class Telemetry { + + private reporter: TelemetryReporter; + private telemetry_key: string = undefined; + + private getExtensionVersion(): string { + return vscode.extensions.getExtension(EXTENSION_ID).packageJSON.version; + } + + private getTelemetryKey(): string { + if (this.telemetry_key === undefined) + this.telemetry_key = Buffer.from(TELEMETRY_KEY_ENCODED, "base64").toString().trim(); + return this.telemetry_key; + } + + constructor() { + this.reporter = new TelemetryReporter(EXTENSION_ID, this.getExtensionVersion(), this.getTelemetryKey()); + } + + public reportEvent(eventName: string, properties? : any, measurements? : any): void { + if (this.isValidTelemetryKey()) { + this.reporter.sendTelemetryEvent(eventName, properties, measurements); + } + } + + public dispose(): any { + this.reporter.dispose(); + } + + private isValidTelemetryKey(): boolean { + return this.getTelemetryKey() !== TELEMETRY_DEFAULT_KEY; + } +} diff --git a/language_server/src/CMakeLists.txt b/language_server/src/CMakeLists.txt index 4483f8921..8997b778e 100644 --- a/language_server/src/CMakeLists.txt +++ b/language_server/src/CMakeLists.txt @@ -26,6 +26,8 @@ target_sources(language_server_base PRIVATE logger.h message_router.cpp message_router.h + parsing_metadata_collector.h + parsing_metadata_serialization.h request_manager.cpp request_manager.h scope_exit.h @@ -33,6 +35,9 @@ target_sources(language_server_base PRIVATE server.h server_streams.h stream_helper.h + telemetry_broker.h + telemetry_info.h + telemetry_sink.h ) if(EMSCRIPTEN) diff --git a/language_server/src/common_types.h b/language_server/src/common_types.h index c461b3d86..2f64baeaa 100644 --- a/language_server/src/common_types.h +++ b/language_server/src/common_types.h @@ -12,13 +12,29 @@ * Broadcom, Inc. - initial API and implementation */ +#ifndef HLASMPLUGIN_LANGUAGESERVER_COMMON_TYPES_H +#define HLASMPLUGIN_LANGUAGESERVER_COMMON_TYPES_H + #include "nlohmann/json.hpp" // Types that are used throughout the language server component namespace hlasm_plugin::language_server { using json = nlohmann::json; -using method = std::function; +enum class telemetry_log_level +{ + NO_TELEMETRY, + LOG_EVENT, + LOG_WITH_PARSE_DATA +}; + +struct method +{ + std::function handler; + telemetry_log_level telemetry_level; +}; using send_message_callback = std::function; } // namespace hlasm_plugin::language_server + +#endif \ No newline at end of file diff --git a/language_server/src/dap/dap_feature.cpp b/language_server/src/dap/dap_feature.cpp index 007bf193d..36cf389ed 100644 --- a/language_server/src/dap/dap_feature.cpp +++ b/language_server/src/dap/dap_feature.cpp @@ -61,22 +61,26 @@ dap_feature::dap_feature(parser_library::workspace_manager& ws_mngr, void dap_feature::register_methods(std::map& methods) { - const auto this_bind = [this](void (dap_feature::*func)(const json&, const json&)) { - return [this, func](const json& requested_seq, const json& args) { (this->*func)(requested_seq, args); }; + const auto this_bind = [this](void (dap_feature::*func)(const json&, const json&), + telemetry_log_level telem = telemetry_log_level::NO_TELEMETRY) { + return method { + [this, func](const json& requested_seq, const json& args) { (this->*func)(requested_seq, args); }, telem + }; }; methods.try_emplace("initialize", this_bind(&dap_feature::on_initialize)); - methods.try_emplace("disconnect", this_bind(&dap_feature::on_disconnect)); - methods.try_emplace("launch", this_bind(&dap_feature::on_launch)); - methods.try_emplace("setBreakpoints", this_bind(&dap_feature::on_set_breakpoints)); - methods.try_emplace("setExceptionBreakpoints", this_bind(&dap_feature::on_set_exception_breakpoints)); + methods.try_emplace("disconnect", this_bind(&dap_feature::on_disconnect, telemetry_log_level::LOG_EVENT)); + methods.try_emplace("launch", this_bind(&dap_feature::on_launch, telemetry_log_level::LOG_EVENT)); + methods.try_emplace("setBreakpoints", this_bind(&dap_feature::on_set_breakpoints, telemetry_log_level::LOG_EVENT)); + methods.try_emplace("setExceptionBreakpoints", + this_bind(&dap_feature::on_set_exception_breakpoints, telemetry_log_level::LOG_EVENT)); methods.try_emplace("configurationDone", this_bind(&dap_feature::on_configuration_done)); methods.try_emplace("threads", this_bind(&dap_feature::on_threads)); methods.try_emplace("stackTrace", this_bind(&dap_feature::on_stack_trace)); methods.try_emplace("scopes", this_bind(&dap_feature::on_scopes)); - methods.try_emplace("next", this_bind(&dap_feature::on_next)); - methods.try_emplace("stepIn", this_bind(&dap_feature::on_step_in)); + methods.try_emplace("next", this_bind(&dap_feature::on_next, telemetry_log_level::LOG_EVENT)); + methods.try_emplace("stepIn", this_bind(&dap_feature::on_step_in, telemetry_log_level::LOG_EVENT)); methods.try_emplace("variables", this_bind(&dap_feature::on_variables)); - methods.try_emplace("continue", this_bind(&dap_feature::on_continue)); + methods.try_emplace("continue", this_bind(&dap_feature::on_continue, telemetry_log_level::LOG_EVENT)); } json dap_feature::register_capabilities() { return json(); } diff --git a/language_server/src/dap/dap_server.cpp b/language_server/src/dap/dap_server.cpp index e278a4a61..a4c8ccef4 100644 --- a/language_server/src/dap/dap_server.cpp +++ b/language_server/src/dap/dap_server.cpp @@ -20,8 +20,8 @@ namespace hlasm_plugin::language_server::dap { -server::server(parser_library::workspace_manager& ws_mngr) - : language_server::server(ws_mngr) +server::server(parser_library::workspace_manager& ws_mngr, telemetry_sink* telemetry_reporter) + : language_server::server(ws_mngr, telemetry_reporter) { features_.push_back(std::make_unique(ws_mngr_, *this, this)); register_feature_methods(); @@ -71,6 +71,7 @@ void server::message_received(const json& message) if (message.at("type") != "request") { LOG_WARNING(std::string("Invalid message receive: ") + message.dump()); + send_telemetry_error("dap_server/invalid_message"); return; } auto arguments = message.find("arguments"); @@ -83,6 +84,7 @@ void server::message_received(const json& message) { (void)e; LOG_WARNING(std::string("There was an error with received request:") + e.what()); + send_telemetry_error("dap_server/method_unknown_error"); } } diff --git a/language_server/src/dap/dap_server.h b/language_server/src/dap/dap_server.h index 8146ace9b..cd8379ce9 100644 --- a/language_server/src/dap/dap_server.h +++ b/language_server/src/dap/dap_server.h @@ -31,7 +31,7 @@ namespace hlasm_plugin::language_server::dap { class server final : public hlasm_plugin::language_server::server, public dap_disconnect_listener { public: - explicit server(parser_library::workspace_manager& ws_mngr); + explicit server(parser_library::workspace_manager& ws_mngr, telemetry_sink* telemetry_reporter = nullptr); void request(const json& id, const std::string& requested_method, const json& args, method handler) override; diff --git a/language_server/src/dap/dap_session.cpp b/language_server/src/dap/dap_session.cpp index 73a988520..9c06a0f94 100644 --- a/language_server/src/dap/dap_session.cpp +++ b/language_server/src/dap/dap_session.cpp @@ -29,7 +29,7 @@ void session::thread_routine() scope_exit indicate_end([this]() { running = false; }); request_manager req_mgr(&cancel); scope_exit end_request_manager([&req_mgr]() { req_mgr.end_worker(); }); - dap::server server(*ws_mngr); + dap::server server(*ws_mngr, telemetry_reporter); dispatcher dispatcher(json_channel_adapter(msg_unwrapper, msg_wrapper), server, req_mgr); dispatcher.run_server_loop(); } @@ -42,11 +42,13 @@ void session::thread_routine() LOG_ERROR("DAP Thread encountered an unknown exception."); } } -session::session(size_t s_id, hlasm_plugin::parser_library::workspace_manager& ws, json_sink& out) +session::session( + size_t s_id, hlasm_plugin::parser_library::workspace_manager& ws, json_sink& out, telemetry_sink* telem_reporter) : session_id(message_wrapper::generate_method_name(s_id)) , ws_mngr(&ws) , msg_wrapper(out, s_id) , msg_unwrapper(queue) + , telemetry_reporter(telem_reporter) { worker = std::thread([this]() { this->thread_routine(); }); } diff --git a/language_server/src/dap/dap_session.h b/language_server/src/dap/dap_session.h index 99bc05893..b32b7b0db 100644 --- a/language_server/src/dap/dap_session.h +++ b/language_server/src/dap/dap_session.h @@ -21,6 +21,7 @@ #include "../json_queue_channel.h" #include "../message_router.h" #include "dap_message_wrappers.h" +#include "telemetry_sink.h" #include "workspace_manager.h" namespace hlasm_plugin::language_server::dap { @@ -34,11 +35,15 @@ class session final : public json_sink dap::message_unwrapper msg_unwrapper; std::thread worker; std::atomic running = true; + telemetry_sink* telemetry_reporter; void thread_routine(); public: - session(size_t session_id, hlasm_plugin::parser_library::workspace_manager& ws, json_sink& out); + session(size_t session_id, + hlasm_plugin::parser_library::workspace_manager& ws, + json_sink& out, + telemetry_sink* telemetry_reporter = nullptr); ~session(); message_router::message_predicate get_message_matcher() const; diff --git a/language_server/src/dap/dap_session_manager.cpp b/language_server/src/dap/dap_session_manager.cpp index a9b56f91b..e8c8bb9c4 100644 --- a/language_server/src/dap/dap_session_manager.cpp +++ b/language_server/src/dap/dap_session_manager.cpp @@ -51,12 +51,14 @@ void session_manager::handle_registration_request(size_t new_id) cleanup_sessions(); // currently only one debug session is supported - auto new_session = std::make_unique(new_id, *ws_mngr, *out_stream); + auto new_session = std::make_unique(new_id, *ws_mngr, *out_stream, telemetry_reporter); sessions.try_emplace(new_session->get_session_id(), std::move(new_session)); } -session_manager::session_manager(hlasm_plugin::parser_library::workspace_manager& ws, json_sink& out) +session_manager::session_manager( + hlasm_plugin::parser_library::workspace_manager& ws, json_sink& out, telemetry_sink* telem_reporter) : ws_mngr(&ws) , out_stream(&out) + , telemetry_reporter(telem_reporter) {} // Inherited via json_sink diff --git a/language_server/src/dap/dap_session_manager.h b/language_server/src/dap/dap_session_manager.h index 12ec4397c..9f803b310 100644 --- a/language_server/src/dap/dap_session_manager.h +++ b/language_server/src/dap/dap_session_manager.h @@ -30,12 +30,15 @@ class session_manager final : public json_sink hlasm_plugin::parser_library::workspace_manager* ws_mngr; json_sink* out_stream; std::map, std::less<>> sessions; + telemetry_sink* telemetry_reporter; void cleanup_sessions(); void handle_registration_request(size_t new_id); public: - session_manager(hlasm_plugin::parser_library::workspace_manager& ws, json_sink& out); + session_manager(hlasm_plugin::parser_library::workspace_manager& ws, + json_sink& out, + telemetry_sink* telemetry_reporter = nullptr); // Inherited via json_sink void write(const nlohmann::json& msg) override; diff --git a/language_server/src/lsp/feature_language_features.cpp b/language_server/src/lsp/feature_language_features.cpp index 1d502299f..e38f5e70e 100644 --- a/language_server/src/lsp/feature_language_features.cpp +++ b/language_server/src/lsp/feature_language_features.cpp @@ -26,18 +26,21 @@ feature_language_features::feature_language_features( void feature_language_features::register_methods(std::map& methods) { - methods.emplace("textDocument/definition", - std::bind(&feature_language_features::definition, this, std::placeholders::_1, std::placeholders::_2)); - methods.emplace("textDocument/references", - std::bind(&feature_language_features::references, this, std::placeholders::_1, std::placeholders::_2)); - methods.emplace("textDocument/hover", - std::bind(&feature_language_features::hover, this, std::placeholders::_1, std::placeholders::_2)); - methods.emplace("textDocument/completion", - std::bind(&feature_language_features::completion, this, std::placeholders::_1, std::placeholders::_2)); + const auto this_bind = [this](void (feature_language_features::*func)(const json&, const json&), + telemetry_log_level telem) { + return method { [this, func](const json& id, const json& args) { (this->*func)(id, args); }, telem }; + }; + methods.emplace( + "textDocument/definition", this_bind(&feature_language_features::definition, telemetry_log_level::LOG_EVENT)); + methods.emplace( + "textDocument/references", this_bind(&feature_language_features::references, telemetry_log_level::LOG_EVENT)); + methods.emplace("textDocument/hover", this_bind(&feature_language_features::hover, telemetry_log_level::LOG_EVENT)); + methods.emplace( + "textDocument/completion", this_bind(&feature_language_features::completion, telemetry_log_level::LOG_EVENT)); methods.emplace("textDocument/semanticTokens/full", - std::bind(&feature_language_features::semantic_tokens, this, std::placeholders::_1, std::placeholders::_2)); + this_bind(&feature_language_features::semantic_tokens, telemetry_log_level::NO_TELEMETRY)); methods.emplace("textDocument/documentSymbol", - std::bind(&feature_language_features::document_symbol, this, std::placeholders::_1, std::placeholders::_2)); + this_bind(&feature_language_features::document_symbol, telemetry_log_level::NO_TELEMETRY)); } json feature_language_features::register_capabilities() diff --git a/language_server/src/lsp/feature_text_synchronization.cpp b/language_server/src/lsp/feature_text_synchronization.cpp index 2ac129aa4..1b1213c61 100644 --- a/language_server/src/lsp/feature_text_synchronization.cpp +++ b/language_server/src/lsp/feature_text_synchronization.cpp @@ -25,26 +25,25 @@ feature_text_synchronization::feature_text_synchronization( void feature_text_synchronization::register_methods(std::map& methods) { - methods.emplace("textDocument/didOpen", - std::bind(&feature_text_synchronization::on_did_open, this, std::placeholders::_1, std::placeholders::_2)); - methods.emplace("textDocument/didChange", - std::bind(&feature_text_synchronization::on_did_change, this, std::placeholders::_1, std::placeholders::_2)); - methods.emplace("textDocument/didClose", - std::bind(&feature_text_synchronization::on_did_close, this, std::placeholders::_1, std::placeholders::_2)); + methods.try_emplace("textDocument/didOpen", + method { [this](const json& id, const json& args) { on_did_open(id, args); }, + telemetry_log_level::LOG_WITH_PARSE_DATA }); + methods.try_emplace("textDocument/didChange", + method { + [this](const json& id, const json& args) { on_did_change(id, args); }, telemetry_log_level::NO_TELEMETRY }); + methods.try_emplace("textDocument/didClose", + method { + [this](const json& id, const json& args) { on_did_close(id, args); }, telemetry_log_level::LOG_EVENT }); } json feature_text_synchronization::register_capabilities() { - // there is no reason why not ask for notifications (most of them is - // ignored anyway). - // we cant process willSaveWaitUntil because it is a request and we dont - // want many hanging requests return json { { "textDocumentSync", json { { "openClose", true }, { "change", (int)text_document_sync_kind::incremental }, - { "willSave", true }, + { "willSave", false }, { "willSaveWaitUntil", false }, - { "save", json { { "includeText", true } } } } } }; + { "save", false } } } }; } diff --git a/language_server/src/lsp/feature_workspace_folders.cpp b/language_server/src/lsp/feature_workspace_folders.cpp index e3067e3b9..03c8c757c 100644 --- a/language_server/src/lsp/feature_workspace_folders.cpp +++ b/language_server/src/lsp/feature_workspace_folders.cpp @@ -32,17 +32,15 @@ feature_workspace_folders::feature_workspace_folders( void feature_workspace_folders::register_methods(std::map& methods) { - methods.emplace("workspace/didChangeWorkspaceFolders", - std::bind(&feature_workspace_folders::on_did_change_workspace_folders, - this, - std::placeholders::_1, - std::placeholders::_2)); - methods.emplace("workspace/didChangeWatchedFiles", - std::bind( - &feature_workspace_folders::did_change_watched_files, this, std::placeholders::_1, std::placeholders::_2)); - methods.emplace("workspace/didChangeConfiguration", - std::bind( - &feature_workspace_folders::did_change_configuration, this, std::placeholders::_1, std::placeholders::_2)); + methods.try_emplace("workspace/didChangeWorkspaceFolders", + method { [this](const json& id, const json& args) { on_did_change_workspace_folders(id, args); }, + telemetry_log_level::LOG_EVENT }); + methods.try_emplace("workspace/didChangeWatchedFiles", + method { [this](const json& id, const json& args) { did_change_watched_files(id, args); }, + telemetry_log_level::NO_TELEMETRY }); + methods.try_emplace("workspace/didChangeConfiguration", + method { [this](const json& id, const json& args) { did_change_configuration(id, args); }, + telemetry_log_level::NO_TELEMETRY }); } json feature_workspace_folders::register_capabilities() @@ -151,7 +149,7 @@ void feature_workspace_folders::send_configuration_request() response_->request("config_request_" + std::to_string(config_request_number_), "workspace/configuration", config_request_args, - std::bind(&feature_workspace_folders::configuration, this, std::placeholders::_1, std::placeholders::_2)); + { [this](const json& id, const json& params) { configuration(id, params); }, telemetry_log_level::LOG_EVENT }); ++config_request_number_; } diff --git a/language_server/src/lsp/lsp_server.cpp b/language_server/src/lsp/lsp_server.cpp index 6c4ff1258..beae6e4a4 100644 --- a/language_server/src/lsp/lsp_server.cpp +++ b/language_server/src/lsp/lsp_server.cpp @@ -23,11 +23,12 @@ #include "feature_text_synchronization.h" #include "feature_workspace_folders.h" #include "lib_config.h" +#include "parsing_metadata_serialization.h" namespace hlasm_plugin::language_server::lsp { server::server(parser_library::workspace_manager& ws_mngr) - : language_server::server(ws_mngr) + : language_server::server(ws_mngr, this) { features_.push_back(std::make_unique(ws_mngr_, *this)); features_.push_back(std::make_unique(ws_mngr_, *this)); @@ -37,6 +38,8 @@ server::server(parser_library::workspace_manager& ws_mngr) ws_mngr_.register_diagnostics_consumer(this); ws_mngr_.set_message_consumer(this); + + ws_mngr_.register_parsing_metadata_consumer(&parsing_metadata_); } void server::message_received(const json& message) @@ -53,6 +56,7 @@ void server::message_received(const json& message) if (id_found == message.end()) { LOG_WARNING("A response with no id field received."); + send_telemetry_error("lsp_server/response_no_id"); return; } @@ -60,12 +64,13 @@ void server::message_received(const json& message) if (handler_found == request_handlers_.end()) { LOG_WARNING("A response with no registered handler received."); + send_telemetry_error("lsp_server/response_no_handler"); return; } method handler = handler_found->second; request_handlers_.erase(handler_found); - handler(id_found.value(), result_found.value()); + handler.handler(id_found.value(), result_found.value()); return; } else if (error_result_found != message.end()) @@ -77,6 +82,7 @@ void server::message_received(const json& message) else warn_message = "Request with id " + id_found->dump() + " returned with unspecified error."; LOG_WARNING(warn_message); + send_telemetry_error("lsp_server/response_error_returned", warn_message); return; } @@ -86,6 +92,7 @@ void server::message_received(const json& message) if (params_found == message.end() || method_found == message.end()) { LOG_WARNING("Method or params missing from received request or notification"); + send_telemetry_error("lsp_server/method_or_params_missing"); return; } @@ -105,10 +112,16 @@ void server::message_received(const json& message) catch (const std::exception& e) { LOG_ERROR(e.what()); + send_telemetry_error("lsp_server/method_unknown_error"); return; } } +telemetry_metrics_info server::get_telemetry_details() +{ + return telemetry_metrics_info { parsing_metadata_.data, diags_error_count, diags_warning_count }; +} + void server::request(const json& id, const std::string& requested_method, const json& args, method handler) { json reply { { "jsonrpc", "2.0" }, { "id", id }, { "method", requested_method }, { "params", args } }; @@ -139,12 +152,27 @@ void server::respond_error( void server::register_methods() { - methods_.emplace( - "initialize", std::bind(&server::on_initialize, this, std::placeholders::_1, std::placeholders::_2)); - methods_.emplace("shutdown", std::bind(&server::on_shutdown, this, std::placeholders::_1, std::placeholders::_2)); - methods_.emplace("exit", std::bind(&server::on_exit, this, std::placeholders::_1, std::placeholders::_2)); + methods_.try_emplace("initialize", + method { [this](const json& id, const json& params) { on_initialize(id, params); }, + telemetry_log_level::LOG_EVENT }); + methods_.try_emplace("initialized", + method { [](const json&, const json&) { /*no implementation, silences uninteresting telemetry*/ }, + telemetry_log_level::NO_TELEMETRY }); + methods_.try_emplace("shutdown", + method { [this](const json& id, const json& params) { on_shutdown(id, params); }, + telemetry_log_level::NO_TELEMETRY }); + methods_.try_emplace("exit", + method { + [this](const json& id, const json& params) { on_exit(id, params); }, telemetry_log_level::NO_TELEMETRY }); + methods_.try_emplace("$/setTraceNotification", + method { [](const json&, const json&) { + /*no implementation, silences reporting of VS Code implementation-specific notification*/ + }, + telemetry_log_level::NO_TELEMETRY }); } +void server::send_telemetry(const telemetry_message& message) { notify("telemetry/event", json(message)); } + void empty_handler(json, const json&) { // Does nothing @@ -157,10 +185,7 @@ void server::on_initialize(json id, const json& param) json { { "documentFormattingProvider", false }, { "documentRangeFormattingProvider", false }, { "codeActionProvider", false }, - { "signatureHelpProvider", - json { - { "triggerCharacters", { "(", "," } }, - } }, + { "signatureHelpProvider", false }, { "documentHighlightProvider", false }, { "renameProvider", false }, { "workspaceSymbolProvider", false } } } }; @@ -177,7 +202,10 @@ void server::on_initialize(json id, const json& param) { { "registrations", { { { "id", "configureRegister" }, { "method", "workspace/didChangeConfiguration" } } } } } }; - request("register1", "client/registerCapability", register_configuration_changed_args, &empty_handler); + request("register1", + "client/registerCapability", + register_configuration_changed_args, + { &empty_handler, telemetry_log_level::NO_TELEMETRY }); for (auto& f : features_) @@ -222,10 +250,17 @@ void server::consume_diagnostics(parser_library::diagnostic_list diagnostics) // map of all diagnostics that came from the server std::map> diags; + diags_error_count = 0; + diags_warning_count = 0; + for (size_t i = 0; i < diagnostics.diagnostics_size(); ++i) { auto d = diagnostics.diagnostics(i); diags[d.file_name()].push_back(d); + if (d.severity() == parser_library::diagnostic_severity::error) + ++diags_error_count; + else if (d.severity() == parser_library::diagnostic_severity::warning) + ++diags_warning_count; } // set of all files for which diagnostics came from the server. diff --git a/language_server/src/lsp/lsp_server.h b/language_server/src/lsp/lsp_server.h index 9fb0c2d4f..03f972aa3 100644 --- a/language_server/src/lsp/lsp_server.h +++ b/language_server/src/lsp/lsp_server.h @@ -21,8 +21,10 @@ #include "../common_types.h" #include "../feature.h" +#include "../parsing_metadata_collector.h" #include "../server.h" #include "nlohmann/json.hpp" +#include "telemetry_sink.h" #include "workspace_manager.h" namespace hlasm_plugin::language_server::lsp { @@ -32,7 +34,8 @@ namespace hlasm_plugin::language_server::lsp { // Consumes diagnostics that come from the parser library and sends them to LSP client. class server final : public hlasm_plugin::language_server::server, public parser_library::diagnostics_consumer, - public parser_library::message_consumer + public parser_library::message_consumer, + public telemetry_sink { public: // Creates the server with workspace_manager as entry point to parser library. @@ -41,6 +44,9 @@ class server final : public hlasm_plugin::language_server::server, // Parses LSP (JSON RPC) message and calls corresponding method. void message_received(const json& message) override; + // Inherited via telemetry_sink + void send_telemetry(const telemetry_message& message) override; + protected: // Sends request to LSP client using send_message_provider. void request(const json& id, const std::string& requested_method, const json& args, method handler) override; @@ -55,9 +61,14 @@ class server final : public hlasm_plugin::language_server::server, const std::string& err_message, const json& error) override; + telemetry_metrics_info get_telemetry_details() override; + private: - // requests + parsing_metadata_collector parsing_metadata_; + size_t diags_warning_count = 0; + size_t diags_error_count = 0; + // requests // Implements initialize request. void on_initialize(json id, const json& param); // Implements the LSP shutdown request. diff --git a/language_server/src/main.cpp b/language_server/src/main.cpp index ae98ac02c..6afe8cdbf 100644 --- a/language_server/src/main.cpp +++ b/language_server/src/main.cpp @@ -25,6 +25,7 @@ #include "message_router.h" #include "scope_exit.h" #include "server_streams.h" +#include "telemetry_broker.h" #include "workspace_manager.h" using namespace hlasm_plugin::language_server; @@ -41,14 +42,14 @@ class main_program : public json_sink message_router router; std::thread lsp_thread; - + telemetry_broker dap_telemetry_broker; dap::session_manager dap_sessions; public: main_program(json_sink& json_output, int& ret) : ws_mngr(&cancel) , router(&lsp_queue) - , dap_sessions(ws_mngr, json_output) + , dap_sessions(ws_mngr, json_output, &dap_telemetry_broker) { router.register_route(dap_sessions.get_filtering_predicate(), dap_sessions); @@ -58,6 +59,8 @@ class main_program : public json_sink request_manager req_mgr(&cancel); scope_exit end_request_manager([&req_mgr]() { req_mgr.end_worker(); }); lsp::server server(ws_mngr); + scope_exit disconnect_telemetry([this]() { dap_telemetry_broker.set_telemetry_sink(nullptr); }); + dap_telemetry_broker.set_telemetry_sink(&server); dispatcher lsp_dispatcher(io, server, req_mgr); ret = lsp_dispatcher.run_server_loop(); diff --git a/language_server/src/parsing_metadata_collector.h b/language_server/src/parsing_metadata_collector.h new file mode 100644 index 000000000..8554ccdf9 --- /dev/null +++ b/language_server/src/parsing_metadata_collector.h @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2021 Broadcom. + * The term "Broadcom" refers to Broadcom Inc. and/or its subsidiaries. + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Broadcom, Inc. - initial API and implementation + */ + +#ifndef HLASMPLUGIN_LANGUAGESERVER_PARSING_METADATA_SINK_H +#define HLASMPLUGIN_LANGUAGESERVER_PARSING_METADATA_SINK_H + +#include "nlohmann/json.hpp" +#include "workspace_manager.h" + +namespace hlasm_plugin::language_server { + +struct parsing_metadata_collector final : public parser_library::parsing_metadata_consumer +{ + void consume_parsing_metadata(const parser_library::parsing_metadata& metadata) override { data = metadata; } + + parser_library::parsing_metadata data; +}; + +} // namespace hlasm_plugin::language_server + + + +#endif \ No newline at end of file diff --git a/language_server/src/parsing_metadata_serialization.h b/language_server/src/parsing_metadata_serialization.h new file mode 100644 index 000000000..593fe5bcf --- /dev/null +++ b/language_server/src/parsing_metadata_serialization.h @@ -0,0 +1,89 @@ +/* + * Copyright (c) 2021 Broadcom. + * The term "Broadcom" refers to Broadcom Inc. and/or its subsidiaries. + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Broadcom, Inc. - initial API and implementation + */ + +#include "nlohmann/json.hpp" +#include "telemetry_info.h" +#include "workspace_manager.h" + +namespace hlasm_plugin::parser_library { + +void inline to_json(nlohmann::json& j, const parser_library::workspace_file_info& info) +{ + j = nlohmann::json { { "config_parsing", info.config_parsing }, + { "diagnostics_suppressed", info.diagnostics_suppressed }, + { "processor_group_found", info.processor_group_found } }; +} + +void inline to_json(nlohmann::json& j, const parser_library::performance_metrics& metrics) +{ + j = nlohmann::json { { "Open Code Statements", metrics.open_code_statements }, + { "Copy Statements", metrics.copy_statements }, + { "Macro Statements", metrics.macro_statements }, + { "Copy Def Statements", metrics.copy_def_statements }, + { "Macro Def Statements", metrics.macro_def_statements }, + { "Lookahead Statements", metrics.lookahead_statements }, + { "Reparsed Statements", metrics.reparsed_statements }, + { "Continued Statements", metrics.continued_statements }, + { "Non-continued Statements", metrics.non_continued_statements }, + { "Lines", metrics.lines }, + { "Files", metrics.files } }; +} + +void inline to_json(nlohmann::json& j, const parser_library::parsing_metadata& metadata) +{ + j = nlohmann::json { { "properties", metadata.ws_info }, { "measurements", metadata.metrics } }; +} + +} // namespace hlasm_plugin::parser_library + + +namespace hlasm_plugin::language_server { + +void inline to_json(nlohmann::json& j, const telemetry_metrics_info& metrics) +{ + j = metrics.metadata; + j["measurements"]["error_count"] = metrics.error_count; + j["measurements"]["warning_count"] = metrics.warning_count; +} + +void inline to_json(nlohmann::json& j, const telemetry_info& info) +{ + if (info.metrics.has_value()) + j = *info.metrics; + else + j["properties"] = nlohmann::json::value_t::null; + + j["measurements"]["duration"] = info.duration; + j["method_name"] = info.method_name; +} + +void inline to_json(nlohmann::json& j, const telemetry_error& err) +{ + nlohmann::json properties; + properties["error_type"] = err.error_type; + if (err.error_details != "") + properties["error_details"] = err.error_details; + j = nlohmann::json { { "method_name", "server_error" }, { "properties", properties }, { "measurements", {} } }; +} + +void inline to_json(nlohmann::json& j, const telemetry_message& message) +{ + if (std::holds_alternative(message)) + j = std::get(message); + else + j = std::get(message); +} + + +} // namespace hlasm_plugin::language_server diff --git a/language_server/src/server.cpp b/language_server/src/server.cpp index 56d4fb77d..972f96379 100644 --- a/language_server/src/server.cpp +++ b/language_server/src/server.cpp @@ -24,8 +24,9 @@ namespace hlasm_plugin::language_server { -server::server(parser_library::workspace_manager& ws_mngr) +server::server(parser_library::workspace_manager& ws_mngr, telemetry_sink* telemetry_provider) : ws_mngr_(ws_mngr) + , telemetry_provider_(telemetry_provider) {} void server::register_feature_methods() @@ -36,6 +37,8 @@ void server::register_feature_methods() } } + + void server::call_method(const std::string& method, const json& id, const json& args) { if (shutdown_request_received_) @@ -48,12 +51,17 @@ void server::call_method(const std::string& method, const json& id, const json& { try { - (*found).second(id, args); + auto start = std::chrono::steady_clock::now(); + (*found).second.handler(id, args); + std::chrono::duration duration = std::chrono::steady_clock::now() - start; + + telemetry_method_call(method, (*found).second.telemetry_level, duration.count()); } catch (const nlohmann::basic_json<>::exception& e) { (void)e; LOG_WARNING("There is an error regarding the JSON or LSP:" + std::string(e.what())); + send_telemetry_error("call_method/json_error"); } } else @@ -61,9 +69,36 @@ void server::call_method(const std::string& method, const json& id, const json& std::ostringstream ss; ss << "Method " << method << " is not available on this server."; LOG_WARNING(ss.str()); + + send_telemetry_error("method_not_implemented", method); } } +telemetry_metrics_info server::get_telemetry_details() { return {}; } + +void server::send_telemetry_error(std::string where, std::string what) +{ + if (!telemetry_provider_) + return; + + telemetry_provider_->send_telemetry(telemetry_error { where, what }); +} + +void server::telemetry_method_call(const std::string& method_name, telemetry_log_level log_level, double seconds) +{ + if (log_level == telemetry_log_level::NO_TELEMETRY) + return; + if (!telemetry_provider_) + return; + + telemetry_info info { method_name, seconds }; + + if (log_level == telemetry_log_level::LOG_WITH_PARSE_DATA) + info.metrics = get_telemetry_details(); + + telemetry_provider_->send_telemetry(info); +} + bool server::is_exit_notification_received() const { return exit_notification_received_; } void server::set_send_message_provider(send_message_provider* provider) { send_message_ = provider; } diff --git a/language_server/src/server.h b/language_server/src/server.h index b1d8146f5..05c260b76 100644 --- a/language_server/src/server.h +++ b/language_server/src/server.h @@ -15,10 +15,14 @@ #ifndef HLASMPLUGIN_LANGUAGESERVER_SERVER #define HLASMPLUGIN_LANGUAGESERVER_SERVER +#include #include +#include "json_channel.h" + #include "common_types.h" #include "feature.h" +#include "telemetry_sink.h" #include "workspace_manager.h" namespace hlasm_plugin::language_server { @@ -40,7 +44,7 @@ class server : public response_provider public: // Constructs the server with workspace_manager. // All the requests and notifications are passed to the workspace manager - explicit server(parser_library::workspace_manager& ws_mngr); + explicit server(parser_library::workspace_manager& ws_mngr, telemetry_sink* telemetry_provider = nullptr); // Tells the server that a massage was received. The server carries out the notification or request. virtual void message_received(const json& message) = 0; @@ -64,11 +68,19 @@ class server : public response_provider bool exit_notification_received_ = false; parser_library::workspace_manager& ws_mngr_; + telemetry_sink* telemetry_provider_; void register_feature_methods(); // Calls a method that is registered in methods_ with the specified name with arguments and id of request. void call_method(const std::string& method, const json& id, const json& args); + + virtual telemetry_metrics_info get_telemetry_details(); + + void send_telemetry_error(std::string where, std::string what = ""); + +private: + void telemetry_method_call(const std::string& method_name, telemetry_log_level log_level, double seconds); }; } // namespace hlasm_plugin::language_server diff --git a/language_server/src/telemetry_broker.h b/language_server/src/telemetry_broker.h new file mode 100644 index 000000000..e293d4fb4 --- /dev/null +++ b/language_server/src/telemetry_broker.h @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2021 Broadcom. + * The term "Broadcom" refers to Broadcom Inc. and/or its subsidiaries. + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Broadcom, Inc. - initial API and implementation + */ + +#ifndef HLASMPLUGIN_LANGUAGESERVER_TELEMETRY_BROKER_H +#define HLASMPLUGIN_LANGUAGESERVER_TELEMETRY_BROKER_H + +#include + +#include "telemetry_sink.h" + +namespace hlasm_plugin::language_server { + +// The purpose of this class is to send telemetry messages from dap_servers to lsp_server. That requires +// synchronization, because both servers may be created and destructed in parallel. +class telemetry_broker : public telemetry_sink +{ + telemetry_sink* telem_sink = nullptr; + std::mutex write_mutex; + +public: + void send_telemetry(const telemetry_message& message) override + { + std::lock_guard guard(write_mutex); + if (telem_sink) + telem_sink->send_telemetry(message); + } + + void set_telemetry_sink(telemetry_sink* sink) + { + std::lock_guard guard(write_mutex); + telem_sink = sink; + } +}; + +} // namespace hlasm_plugin::language_server + +#endif diff --git a/language_server/src/telemetry_info.h b/language_server/src/telemetry_info.h new file mode 100644 index 000000000..7e4a674ea --- /dev/null +++ b/language_server/src/telemetry_info.h @@ -0,0 +1,51 @@ +/* + * Copyright (c) 2021 Broadcom. + * The term "Broadcom" refers to Broadcom Inc. and/or its subsidiaries. + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Broadcom, Inc. - initial API and implementation + */ + +#ifndef HLASMPLUGIN_LANGUAGESERVER_TELEMETRY_INFO_H +#define HLASMPLUGIN_LANGUAGESERVER_TELEMETRY_INFO_H + +#include +#include +#include + +#include "nlohmann/json.hpp" +#include "workspace_manager.h" + +namespace hlasm_plugin::language_server { + +struct telemetry_metrics_info +{ + parser_library::parsing_metadata metadata; + size_t error_count; + size_t warning_count; +}; + +struct telemetry_info +{ + std::string method_name; + double duration; + std::optional metrics; +}; + +struct telemetry_error +{ + std::string error_type; + std::string error_details; +}; + +using telemetry_message = std::variant; + +} // namespace hlasm_plugin::language_server + +#endif diff --git a/language_server/src/telemetry_sink.h b/language_server/src/telemetry_sink.h new file mode 100644 index 000000000..74c5f9454 --- /dev/null +++ b/language_server/src/telemetry_sink.h @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2021 Broadcom. + * The term "Broadcom" refers to Broadcom Inc. and/or its subsidiaries. + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Broadcom, Inc. - initial API and implementation + */ + +#ifndef HLASMPLUGIN_LANGUAGESERVER_TELEMETRY_SINK_H +#define HLASMPLUGIN_LANGUAGESERVER_TELEMETRY_SINK_H + +#include "telemetry_info.h" + + +namespace hlasm_plugin::language_server { + +class telemetry_sink +{ +public: + virtual void send_telemetry(const telemetry_message& message) = 0; + +protected: + ~telemetry_sink() = default; +}; + +} // namespace hlasm_plugin::language_server + +#endif diff --git a/language_server/test/CMakeLists.txt b/language_server/test/CMakeLists.txt index b8f47f9aa..7da7d61f7 100644 --- a/language_server/test/CMakeLists.txt +++ b/language_server/test/CMakeLists.txt @@ -23,6 +23,7 @@ target_sources(server_test PRIVATE send_message_provider_mock.h stream_helper_test.cpp ws_mngr_mock.h + telemetry_test.cpp ) add_subdirectory(dap) diff --git a/language_server/test/dispatcher_test.cpp b/language_server/test/dispatcher_test.cpp index 9eff629a5..6924dd865 100644 --- a/language_server/test/dispatcher_test.cpp +++ b/language_server/test/dispatcher_test.cpp @@ -21,16 +21,17 @@ #include "stream_helper.h" +using namespace hlasm_plugin; using namespace hlasm_plugin::language_server; class server_mock : public server { - hlasm_plugin::parser_library::workspace_manager ws; + ; int counter = 0; int messages_limit; public: - server_mock(int max_messages) + server_mock(parser_library::workspace_manager& ws, int max_messages) : server(ws) , messages_limit(max_messages) {} @@ -125,7 +126,8 @@ TEST_P(dispatcher_fixture, basic) std::atomic cancel = false; request_manager rm(&cancel, request_manager::async_policy::SYNC); - server_mock dummy_server(GetParam().messages_limit); + parser_library::workspace_manager ws; + server_mock dummy_server(ws, GetParam().messages_limit); base_protocol_channel channel(ss_in, ss_out); dispatcher disp(json_channel_adapter(channel), dummy_server, rm); @@ -139,8 +141,9 @@ TEST_P(dispatcher_fixture, basic) TEST(dispatcher, write_message) { + parser_library::workspace_manager ws; std::stringstream ss; - server_mock dummy_server(1); + server_mock dummy_server(ws, 1); std::atomic cancel = false; request_manager rm(&cancel, request_manager::async_policy::SYNC); base_protocol_channel channel(ss, ss); diff --git a/language_server/test/lsp/feature_language_features_test.cpp b/language_server/test/lsp/feature_language_features_test.cpp index be63cbb4d..4707037a8 100644 --- a/language_server/test/lsp/feature_language_features_test.cpp +++ b/language_server/test/lsp/feature_language_features_test.cpp @@ -45,7 +45,7 @@ TEST(language_features, completion) EXPECT_CALL(ws_mngr, completion( StrEq(path), parser_library::position(0, 1), '\0', parser_library::completion_trigger_kind::invoked)); - notifs["textDocument/completion"]("", params1); + notifs["textDocument/completion"].handler("", params1); } TEST(language_features, hover) @@ -64,7 +64,7 @@ TEST(language_features, hover) std::string s("test"); std::string_view ret(s); EXPECT_CALL(ws_mngr, hover(StrEq(path), parser_library::position(0, 1))).WillOnce(Return(ret)); - notifs["textDocument/hover"]("", params1); + notifs["textDocument/hover"].handler("", params1); } TEST(language_features, definition) @@ -84,7 +84,7 @@ TEST(language_features, definition) : R"({"textDocument":{"uri":"file:///home/test"},"position":{"line":0,"character":1}})"_json; EXPECT_CALL(response_mock, respond(json(""), "", ::testing::_)); - notifs["textDocument/definition"]("", params1); + notifs["textDocument/definition"].handler("", params1); } TEST(language_features, references) @@ -101,7 +101,7 @@ TEST(language_features, references) : R"({"textDocument":{"uri":"file:///home/test"},"position":{"line":0,"character":1}})"_json; EXPECT_CALL(ws_mngr, references(StrEq(path), parser_library::position(0, 1))); - notifs["textDocument/references"]("", params1); + notifs["textDocument/references"].handler("", params1); } TEST(language_features, document_symbol) @@ -122,7 +122,7 @@ TEST(language_features, document_symbol) response.push_back( { { "name", "A" }, { "kind", 17 }, { "range", r }, { "selectionRange", r }, { "children", json::array() } }); EXPECT_CALL(response_mock, respond(json(""), std::string(""), response)); - notifs["textDocument/documentSymbol"]("", params1); + notifs["textDocument/documentSymbol"].handler("", params1); } TEST(language_features, semantic_tokens) @@ -141,7 +141,7 @@ TEST(language_features, semantic_tokens) json response { { "data", { 0, 0, 1, 0, 0, 0, 2, 3, 1, 0, 0, 4, 1, 10, 0, 1, 1, 5, 1, 0 } } }; EXPECT_CALL(response_mock, respond(json(""), std::string(""), response)); - notifs["textDocument/semanticTokens/full"]("", params1); + notifs["textDocument/semanticTokens/full"].handler("", params1); } TEST(language_features, semantic_tokens_multiline) @@ -174,7 +174,7 @@ IIIIIIIIIIIIIII1 // clang-format on EXPECT_CALL(response_mock, respond(json(""), std::string(""), response)); - notifs["textDocument/semanticTokens/full"]("", params1); + notifs["textDocument/semanticTokens/full"].handler("", params1); } #endif diff --git a/language_server/test/lsp/feature_text_synchronization_test.cpp b/language_server/test/lsp/feature_text_synchronization_test.cpp index e7d04adce..766ca0bf7 100644 --- a/language_server/test/lsp/feature_text_synchronization_test.cpp +++ b/language_server/test/lsp/feature_text_synchronization_test.cpp @@ -45,7 +45,7 @@ TEST(text_synchronization, did_open_file) EXPECT_CALL(ws_mngr, did_open_file(StrEq(txt_file_path), 4, StrEq("sad"), 3)); - notifs["textDocument/didOpen"]("", params1); + notifs["textDocument/didOpen"].handler("", params1); } MATCHER_P2(PointerAndSizeEqArray, pointer, size, "") @@ -79,7 +79,7 @@ TEST(text_synchronization, did_change_file) EXPECT_CALL(ws_mngr, did_change_file(StrEq(txt_file_path), 7, _, 2)) .With(Args<2, 3>(PointerAndSizeEqArray(expected1, std::size(expected1)))); - notifs["textDocument/didChange"]("", params1); + notifs["textDocument/didChange"].handler("", params1); @@ -89,14 +89,14 @@ TEST(text_synchronization, did_change_file) EXPECT_CALL(ws_mngr, did_change_file(StrEq(txt_file_path), 7, _, 1)) .With(Args<2, 3>(PointerAndSizeEqArray(expected2, std::size(expected2)))); - notifs["textDocument/didChange"]("", params2); + notifs["textDocument/didChange"].handler("", params2); json params3 = json::parse(R"({"textDocument":{"uri":")" + txt_file_uri + R"("},"contentChanges":[{"range":{"start":{"line":0,"character":0},"end":{"line":0,"character":8}},"rangeLength":8,"text":"sad"}, {"range":{"start":{"line":1,"character":12},"end":{"line":1,"character":14}},"rangeLength":2,"text":""}]})"); - EXPECT_THROW(notifs["textDocument/didChange"]("", params3), nlohmann::basic_json<>::exception); + EXPECT_THROW(notifs["textDocument/didChange"].handler("", params3), nlohmann::basic_json<>::exception); } TEST(text_synchronization, did_close_file) @@ -111,7 +111,7 @@ TEST(text_synchronization, did_close_file) json params1 = json::parse(R"({"textDocument":{"uri":")" + txt_file_uri + R"("}})"); EXPECT_CALL(ws_mngr, did_close_file(StrEq(txt_file_path))), - notifs["textDocument/didClose"]("", params1); + notifs["textDocument/didClose"].handler("", params1); } TEST(feature, uri_to_path) diff --git a/language_server/test/lsp/lsp_server_test.cpp b/language_server/test/lsp/lsp_server_test.cpp index 79b981b77..4da371d6b 100644 --- a/language_server/test/lsp/lsp_server_test.cpp +++ b/language_server/test/lsp/lsp_server_test.cpp @@ -59,6 +59,9 @@ TEST(lsp_server, initialize) EXPECT_CALL(smpm, reply(show_message)).Times(::testing::AtMost(1)); EXPECT_CALL(smpm, reply(register_message)).Times(::testing::AtMost(1)); EXPECT_CALL(smpm, reply(config_request_message)).Times(::testing::AtMost(1)); + EXPECT_CALL(smpm, reply(::testing::Truly([](const json& arg) { + return arg.count("method") && arg["method"] == "telemetry/event"; + }))); s.message_received(j); @@ -94,6 +97,16 @@ TEST(lsp_server, not_implemented_method) lsp::server s(ws_mngr); s.set_send_message_provider(&smpm); + json expected_telemetry = + R"({"jsonrpc":"2.0","method":"telemetry/event","params":{ + "measurements":null, + "method_name":"server_error", + "properties":{"error_details":"unknown_method","error_type":"method_not_implemented"} + }})"_json; + + // Only telemetry expected + EXPECT_CALL(smpm, reply(expected_telemetry)); + s.message_received(j); // No result is tested, server should ignore unknown LSP method } @@ -129,7 +142,8 @@ TEST(lsp_server, request_correct) rp.request("a_request", "client_method", "a_json_parameter", - std::bind(&request_handler::handle, &handler, std::placeholders::_1, std::placeholders::_2)); + { [&handler](const json& id, const json& params) { handler.handle(id, params); }, + telemetry_log_level::NO_TELEMETRY }); json request_response = R"({"id":"a_request","jsonrpc":"2.0","result":"response_result"})"_json; @@ -149,7 +163,15 @@ TEST(lsp_server, request_no_handler) json request_response = R"({"id":"a_request","jsonrpc":"2.0","result":"response_result"})"_json; - EXPECT_CALL(message_provider, reply(::testing::_)).Times(0); + json expected_telemetry = + R"({"jsonrpc":"2.0","method":"telemetry/event","params":{ + "measurements":null, + "method_name":"server_error", + "properties":{"error_type":"lsp_server/response_no_handler"} + }})"_json; + + // Only telemetry expected + EXPECT_CALL(message_provider, reply(expected_telemetry)); s.message_received(request_response); } @@ -163,7 +185,15 @@ TEST(lsp_server, request_no_id) json request_response = R"({"jsonrpc":"2.0","result":"response_result"})"_json; - EXPECT_CALL(message_provider, reply(::testing::_)).Times(0); + json expected_telemetry = + R"({"jsonrpc":"2.0","method":"telemetry/event","params":{ + "measurements":null, + "method_name":"server_error", + "properties":{"error_type":"lsp_server/response_no_id"} + }})"_json; + + // Only telemetry expected + EXPECT_CALL(message_provider, reply(expected_telemetry)); s.message_received(request_response); } @@ -179,7 +209,16 @@ TEST(lsp_server, request_error) json request_response = R"({"id":"a_request","jsonrpc":"2.0","error":{"message":"the_error_message"}})"_json; - EXPECT_CALL(message_provider, reply(::testing::_)).Times(0); + json expected_telemetry = + R"({"jsonrpc":"2.0","method":"telemetry/event","params":{ + "measurements":null, + "method_name":"server_error", + "properties":{"error_details":"\"the_error_message\"", + "error_type":"lsp_server/response_error_returned"} + }})"_json; + + // Only telemetry expected + EXPECT_CALL(message_provider, reply(expected_telemetry)); s.message_received(request_response); } @@ -193,7 +232,17 @@ TEST(lsp_server, request_error_no_message) json request_response = R"({"id":"a_request","jsonrpc":"2.0","error":null})"_json; - EXPECT_CALL(message_provider, reply(::testing::_)).Times(0); + json expected_telemetry = + R"({"jsonrpc":"2.0","method":"telemetry/event","params":{ + "measurements":null, + "method_name":"server_error", + "properties":{"error_details":"Request with id \"a_request\" returned with unspecified error.", + "error_type":"lsp_server/response_error_returned"} + }})"_json; + + // Only telemetry expected + + EXPECT_CALL(message_provider, reply(expected_telemetry)); s.message_received(request_response); } @@ -205,6 +254,17 @@ TEST(lsp_server_test, wrong_message_received) lsp::server s(ws_mngr); s.set_send_message_provider(&smpm); + json expected_telemetry = + R"({"jsonrpc":"2.0","method":"telemetry/event","params":{ + "measurements":null, + "method_name":"server_error", + "properties":{"error_type":"lsp_server/method_unknown_error"} + }})"_json; + + // Only telemetry expected + + EXPECT_CALL(smpm, reply(expected_telemetry)); + s.message_received( R"({"jsonrpc":"2.0","method":"textDocument/didOpen","params":{"textDocument":{"uri":"user_storage:/user/storage/layout","languageId":"plaintext","version":4,"text":"sad"}}})"_json); } diff --git a/language_server/test/lsp/workspace_folders_test.cpp b/language_server/test/lsp/workspace_folders_test.cpp index 647d74f45..d974bca4b 100644 --- a/language_server/test/lsp/workspace_folders_test.cpp +++ b/language_server/test/lsp/workspace_folders_test.cpp @@ -52,7 +52,7 @@ TEST(workspace_folders, did_change_workspace_folders) json params1 = json::parse(R"({"event":{"added":[{"uri":")" + ws1_uri + R"(","name":"OneDrive"}],"removed":[]}})"); - notifs["workspace/didChangeWorkspaceFolders"]("", params1); + notifs["workspace/didChangeWorkspaceFolders"].handler("", params1); EXPECT_CALL(ws_mngr, add_workspace(::testing::StrEq("TwoDrive"), ::testing::StrEq(ws2_path))); EXPECT_CALL(ws_mngr, add_workspace(::testing::StrEq("ThreeDrive"), ::testing::StrEq(ws3_path))); @@ -60,7 +60,7 @@ TEST(workspace_folders, did_change_workspace_folders) json params2 = json::parse(R"({"event":{"added":[{"uri":")" + ws2_uri + R"(","name":"TwoDrive"},{"uri":")" + ws3_uri + R"(","name":"ThreeDrive"}],"removed":[{"uri":")" + ws1_uri + R"(","name":"OneDrive"}]}})"); - notifs["workspace/didChangeWorkspaceFolders"]("", params2); + notifs["workspace/didChangeWorkspaceFolders"].handler("", params2); EXPECT_CALL(ws_mngr, remove_workspace(::testing::StrEq(ws2_path))); EXPECT_CALL(ws_mngr, remove_workspace(::testing::StrEq(ws3_path))); @@ -68,7 +68,7 @@ TEST(workspace_folders, did_change_workspace_folders) json params3 = json::parse(R"({"event":{"added":[{"uri":")" + ws4_uri + R"(","name":"FourDrive"}],"removed":[{"uri":")" + ws2_uri + R"(","name":"TwoDrive"},{"uri":")" + ws3_uri + R"(","name":"ThreeDrive"}]}})"); - notifs["workspace/didChangeWorkspaceFolders"]("", params3); + notifs["workspace/didChangeWorkspaceFolders"].handler("", params3); } TEST(workspace_folders, did_change_watchedfiles_invalid_uri) @@ -80,7 +80,7 @@ TEST(workspace_folders, did_change_watchedfiles_invalid_uri) std::map notifs; f.register_methods(notifs); - notifs["workspace/didChangeWatchedFiles"]( + notifs["workspace/didChangeWatchedFiles"].handler( "", R"({"changes":[{"uri":"user_storage:/user/storage/layout","type":2}, {"uri":"file:///file_name"}]})"_json); } @@ -171,14 +171,14 @@ TEST(workspace_folders, did_change_configuration) provider, request(json("config_request_0"), "workspace/configuration", config_request_args, ::testing::_)) .WillOnce(::testing::SaveArg<3>(&handler)); - methods["workspace/didChangeConfiguration"]("did_change_configuration_id", "{}"_json); + methods["workspace/didChangeConfiguration"].handler("did_change_configuration_id", "{}"_json); parser_library::lib_config expected_config; expected_config.diag_supress_limit = 42; EXPECT_CALL(ws_mngr, configuration_changed(::testing::Eq(expected_config))); - handler("config_respond", R"([{"diagnosticsSuppressLimit":42}])"_json); + handler.handler("config_respond", R"([{"diagnosticsSuppressLimit":42}])"_json); } TEST(workspace_folders, did_change_configuration_empty_configuration_params) @@ -202,9 +202,9 @@ TEST(workspace_folders, did_change_configuration_empty_configuration_params) provider, request(json("config_request_0"), "workspace/configuration", config_request_args, ::testing::_)) .WillOnce(::testing::SaveArg<3>(&handler)); - methods["workspace/didChangeConfiguration"]("did_change_configuration_id", "{}"_json); + methods["workspace/didChangeConfiguration"].handler("did_change_configuration_id", "{}"_json); EXPECT_CALL(ws_mngr, configuration_changed(::testing::_)).Times(0); - handler("config_respond", R"([])"_json); + handler.handler("config_respond", R"([])"_json); } diff --git a/language_server/test/regress_test.cpp b/language_server/test/regress_test.cpp index 1c6d7afe4..9b4e26407 100644 --- a/language_server/test/regress_test.cpp +++ b/language_server/test/regress_test.cpp @@ -50,9 +50,13 @@ TEST(regress_test, behaviour_error) R"#({"textDocument":{"uri":"file:///c%3A/test/behaviour_error.hlasm","languageId":"plaintext","version":1,"text":"LABEL LR 1,20 REMARK"}})#"_json); s.message_received(notf); - ASSERT_EQ(mess_p.notfs.size(), (size_t)1); - ASSERT_EQ(mess_p.notfs[0]["method"], "textDocument/publishDiagnostics"); - auto diagnostics = mess_p.notfs[0]["params"]["diagnostics"]; + ASSERT_EQ(mess_p.notfs.size(), (size_t)2); + auto publish_notif = std::find_if(mess_p.notfs.begin(), mess_p.notfs.end(), [&](json notif) { + return notif["method"] == "textDocument/publishDiagnostics"; + }); + ASSERT_NE(publish_notif, mess_p.notfs.end()); + ASSERT_EQ((*publish_notif)["method"], "textDocument/publishDiagnostics"); + auto diagnostics = (*publish_notif)["params"]["diagnostics"]; ASSERT_EQ(diagnostics.size(), (size_t)1); EXPECT_EQ(diagnostics[0]["code"].get(), "M120"); diff --git a/language_server/test/request_manager_test.cpp b/language_server/test/request_manager_test.cpp index caaf5b410..08a91cabf 100644 --- a/language_server/test/request_manager_test.cpp +++ b/language_server/test/request_manager_test.cpp @@ -28,8 +28,8 @@ using namespace std::chrono_literals; class server_mock_rm : public server { public: - server_mock_rm(std::atomic* cancel) - : server(ws_mngr_) + server_mock_rm(parser_library::workspace_manager& ws_mngr, std::atomic* cancel) + : server(ws_mngr) , cancel_(cancel) {} void message_received(const json&) override @@ -51,16 +51,16 @@ class server_mock_rm : public server int messages_received = 0; private: - parser_library::workspace_manager ws_mngr_; std::atomic* cancel_; }; TEST(request_manager, finish_requests) { + parser_library::workspace_manager ws_mngr; std::atomic cancel = false; request_manager rm(&cancel); - server_mock_rm s(&cancel); - server_mock_rm s2(&cancel); + server_mock_rm s(ws_mngr, &cancel); + server_mock_rm s2(ws_mngr, &cancel); rm.add_request(&s, "0"_json); rm.add_request(&s, "0"_json); diff --git a/language_server/test/telemetry_test.cpp b/language_server/test/telemetry_test.cpp new file mode 100644 index 000000000..f0ce6ce8c --- /dev/null +++ b/language_server/test/telemetry_test.cpp @@ -0,0 +1,146 @@ +/* + * Copyright (c) 2019 Broadcom. + * The term "Broadcom" refers to Broadcom Inc. and/or its subsidiaries. + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Broadcom, Inc. - initial API and implementation + */ + +#include +#include + +#include "gmock/gmock.h" + +#include "dap/dap_server.h" +#include "lsp/lsp_server.h" +#include "scope_exit.h" +#include "send_message_provider_mock.h" +#include "stream_helper.h" +#include "telemetry_broker.h" +#include "workspace_manager.h" + +using namespace hlasm_plugin; +using namespace hlasm_plugin::language_server; + +using ::testing::SaveArg; +using ::testing::Truly; + +namespace { +json open_file_message = + R"({"jsonrpc":"2.0","id":47,"method":"textDocument/didOpen","params":{"textDocument":{"uri":"file:///test_file","languageId":"hlasm","version":4,"text":" LR 16,1"}}})"_json; + +auto get_method_matcher(std::string method) +{ + return [method = std::move(method)]( + const json& arg) { return arg.count("method") > 0 && arg["method"].get() == method; }; +} + +auto get_telemetry_method_matcher(std::string method) +{ + return [method = std::move(method)](const json& arg) { + return arg.count("method") > 0 && arg["method"].get() == "telemetry/event" + && arg["params"]["method_name"] == method; + }; +} + +} // namespace +TEST(telemetry, lsp_server_did_open) +{ + parser_library::workspace_manager ws_mngr; + lsp::server lsp_server(ws_mngr); + send_message_provider_mock lsp_smpm; + lsp_server.set_send_message_provider(&lsp_smpm); + + + + json diags_reply; + json telemetry_reply; + + + EXPECT_CALL(lsp_smpm, reply(Truly(get_method_matcher("textDocument/publishDiagnostics")))) + .WillOnce(SaveArg<0>(&diags_reply)); + + EXPECT_CALL(lsp_smpm, reply(Truly(get_method_matcher("telemetry/event")))).WillOnce(SaveArg<0>(&telemetry_reply)); + + + + lsp_server.message_received(open_file_message); + + EXPECT_EQ(diags_reply["params"]["diagnostics"].size(), 1); + + json& metrics = telemetry_reply["params"]["measurements"]; + + EXPECT_EQ(metrics["Files"], 1); + EXPECT_GT(metrics["duration"], 0U); + EXPECT_EQ(metrics["error_count"], 1); + + json& ws_info = telemetry_reply["params"]["properties"]; + + EXPECT_EQ(ws_info["diagnostics_suppressed"], false); +} + +TEST(telemetry, telemetry_broker) +{ + parser_library::workspace_manager ws_mngr; + lsp::server lsp_server(ws_mngr); + send_message_provider_mock lsp_smpm; + lsp_server.set_send_message_provider(&lsp_smpm); + + + EXPECT_CALL(lsp_smpm, reply(Truly(get_method_matcher("textDocument/publishDiagnostics")))); + + + EXPECT_CALL(lsp_smpm, reply(Truly(get_telemetry_method_matcher("textDocument/didOpen")))); + + EXPECT_CALL( + lsp_smpm, reply(Truly([](auto& arg) { return arg.count("id") > 0 && arg["id"] == 48; }))); // response to + EXPECT_CALL(lsp_smpm, reply(Truly(get_telemetry_method_matcher("textDocument/hover")))); + EXPECT_CALL(lsp_smpm, reply(Truly(get_telemetry_method_matcher("launch")))); + EXPECT_CALL(lsp_smpm, reply(Truly(get_telemetry_method_matcher("disconnect")))); + + + telemetry_broker broker; + lsp_server.message_received(open_file_message); + + //"textDocument/hover",R"#({"textDocument":{"uri":"file:///c%3A/test/stability.hlasm"},"position":{"line":0,"character":7}})#"_json), + + std::thread lsp_thread([&broker, &lsp_server]() { + broker.set_telemetry_sink(&lsp_server); + + json message; + lsp_server.message_received( + R"({"jsonrpc":"2.0","id":48,"method":"textDocument/hover","params":{"textDocument":{"uri":"file:///test_file"},"position":{"line":0,"character":2} }})"_json); + }); + + + std::thread dap_thread([&]() { + dap::server dap_server(ws_mngr, &broker); + ::testing::NiceMock dap_smpm; + dap_server.set_send_message_provider(&dap_smpm); + + // actual message sent by VS Code + json initialize_message = + R"({"command":"initialize","arguments":{"pathFormat":"uri","linesStartAt1":true,"columnsStartAt1":true},"type":"request","seq":1})"_json; + + dap_server.message_received(initialize_message); + + json launch_message = + R"({"command":"launch","arguments":{"program":"file:///test_file","stopOnEntry":true,"restart":false},"type":"request","seq":10})"_json; + + dap_server.message_received(launch_message); + + json disconnect_message = + R"({"command":"disconnect","arguments":{"restart":false},"type":"request","seq":10})"_json; + + dap_server.message_received(disconnect_message); + }); + + dap_thread.join(); + lsp_thread.join(); +} diff --git a/parser_library/include/protocol.h b/parser_library/include/protocol.h index 7d42ee34a..07c62229b 100644 --- a/parser_library/include/protocol.h +++ b/parser_library/include/protocol.h @@ -260,6 +260,19 @@ struct PARSER_LIBRARY_EXPORT performance_metrics size_t files = 0; }; +struct PARSER_LIBRARY_EXPORT workspace_file_info +{ + bool config_parsing = false; + bool diagnostics_suppressed = false; + bool processor_group_found = false; +}; + +struct PARSER_LIBRARY_EXPORT parsing_metadata +{ + performance_metrics metrics; + workspace_file_info ws_info; +}; + bool inline operator==(const performance_metrics& lhs, const performance_metrics& rhs) { return lhs.lines == rhs.lines && lhs.macro_def_statements == rhs.macro_def_statements diff --git a/parser_library/include/workspace_manager.h b/parser_library/include/workspace_manager.h index 781134b5e..ad1019c7d 100644 --- a/parser_library/include/workspace_manager.h +++ b/parser_library/include/workspace_manager.h @@ -54,13 +54,13 @@ class diagnostics_consumer // Interface that can be implemented to be able to get performance metrics //(time that parsing took, number of parsed lines, etc) -class performance_metrics_consumer +class parsing_metadata_consumer { public: - virtual void consume_performance_metrics(const performance_metrics& metrics) = 0; + virtual void consume_parsing_metadata(const parsing_metadata& metrics) = 0; protected: - ~performance_metrics_consumer() = default; + ~parsing_metadata_consumer() = default; }; // The main class that encapsulates all functionality of parser library. @@ -105,9 +105,11 @@ class PARSER_LIBRARY_EXPORT workspace_manager virtual void configuration_changed(const lib_config& new_config); - // implementation of observer pattern - register consumer. Unregistering not implemented (yet). + // implementation of observer pattern - register consumer. virtual void register_diagnostics_consumer(diagnostics_consumer* consumer); - virtual void register_performance_metrics_consumer(performance_metrics_consumer* consumer); + virtual void unregister_diagnostics_consumer(diagnostics_consumer* consumer); + virtual void register_parsing_metadata_consumer(parsing_metadata_consumer* consumer); + virtual void unregister_parsing_metadata_consumer(parsing_metadata_consumer* consumer); virtual void set_message_consumer(message_consumer* consumer); private: diff --git a/parser_library/src/workspace_manager.cpp b/parser_library/src/workspace_manager.cpp index dceb31a72..dc41a6186 100644 --- a/parser_library/src/workspace_manager.cpp +++ b/parser_library/src/workspace_manager.cpp @@ -89,10 +89,21 @@ void workspace_manager::register_diagnostics_consumer(diagnostics_consumer* cons impl_->register_diagnostics_consumer(consumer); } -void workspace_manager::register_performance_metrics_consumer(performance_metrics_consumer* consumer) +void workspace_manager::unregister_diagnostics_consumer(diagnostics_consumer* consumer) { - impl_->register_performance_metrics_consumer(consumer); + impl_->unregister_diagnostics_consumer(consumer); } + +void workspace_manager::register_parsing_metadata_consumer(parsing_metadata_consumer* consumer) +{ + impl_->register_parsing_metadata_consumer(consumer); +} + +void workspace_manager::unregister_parsing_metadata_consumer(parsing_metadata_consumer* consumer) +{ + impl_->unregister_parsing_metadata_consumer(consumer); +} + void workspace_manager::set_message_consumer(message_consumer* consumer) { impl_->set_message_consumer(consumer); } position_uri workspace_manager::definition(const char* document_uri, const position pos) diff --git a/parser_library/src/workspace_manager_impl.h b/parser_library/src/workspace_manager_impl.h index 6be7b05bb..9f0807bb0 100644 --- a/parser_library/src/workspace_manager_impl.h +++ b/parser_library/src/workspace_manager_impl.h @@ -15,6 +15,8 @@ #ifndef HLASMPLUGIN_PARSERLIBRARY_WORKSPACE_MANAGER_IMPL_H #define HLASMPLUGIN_PARSERLIBRARY_WORKSPACE_MANAGER_IMPL_H +#include + #include "debugging/debug_lib_provider.h" #include "workspace_manager.h" #include "workspaces/file_manager_impl.h" @@ -77,13 +79,13 @@ class workspace_manager::impl final : public diagnosable_impl return; workspaces::workspace& ws = ws_path_match(document_uri); - ws.did_open_file(document_uri); + auto metadata = ws.did_open_file(document_uri); if (cancel_ && *cancel_) return; notify_diagnostics_consumers(); // only on open - notify_performance_consumers(document_uri); + notify_performance_consumers(document_uri, metadata); } void did_change_file( const std::string document_uri, version_t version, const document_change* changes, size_t ch_size) @@ -118,10 +120,21 @@ class workspace_manager::impl final : public diagnosable_impl } void register_diagnostics_consumer(diagnostics_consumer* consumer) { diag_consumers_.push_back(consumer); } + void unregister_diagnostics_consumer(diagnostics_consumer* consumer) + { + diag_consumers_.erase( + std::remove(diag_consumers_.begin(), diag_consumers_.end(), consumer), diag_consumers_.end()); + } + + void register_parsing_metadata_consumer(parsing_metadata_consumer* consumer) + { + parsing_metadata_consumers_.push_back(consumer); + } - void register_performance_metrics_consumer(performance_metrics_consumer* consumer) + void unregister_parsing_metadata_consumer(parsing_metadata_consumer* consumer) { - metrics_consumers_.push_back(consumer); + auto& pmc = parsing_metadata_consumers_; + pmc.erase(std::remove(pmc.begin(), pmc.end(), consumer), pmc.end()); } void set_message_consumer(message_consumer* consumer) @@ -249,16 +262,16 @@ class workspace_manager::impl final : public diagnosable_impl } } - void notify_performance_consumers(const std::string& document_uri) + void notify_performance_consumers(const std::string& document_uri, workspace_file_info ws_file_info) const { auto file = file_manager_.find(document_uri); auto proc_file = dynamic_cast(file.get()); if (proc_file) { auto metrics = proc_file->get_metrics(); - for (auto consumer : metrics_consumers_) + for (auto consumer : parsing_metadata_consumers_) { - consumer->consume_performance_metrics(metrics); + consumer->consume_parsing_metadata({ metrics, ws_file_info }); } } } @@ -284,7 +297,7 @@ class workspace_manager::impl final : public diagnosable_impl std::atomic* cancel_; std::vector diag_consumers_; - std::vector metrics_consumers_; + std::vector parsing_metadata_consumers_; message_consumer* message_consumer_ = nullptr; }; } // namespace hlasm_plugin::parser_library diff --git a/parser_library/src/workspaces/workspace.cpp b/parser_library/src/workspaces/workspace.cpp index 219cb1a26..33767e1b9 100644 --- a/parser_library/src/workspaces/workspace.cpp +++ b/parser_library/src/workspaces/workspace.cpp @@ -148,8 +148,10 @@ const processor_group& workspace::get_proc_grp_by_program(const std::string& fil const ws_uri& workspace::uri() { return uri_; } -void workspace::parse_file(const std::string& file_uri) +workspace_file_info workspace::parse_file(const std::string& file_uri) { + workspace_file_info ws_file_info; + std::filesystem::path file_path(file_uri); // add support for hlasm to vscode (auto detection??) and do the decision based on languageid if (utils::path::equal(file_path, proc_grps_path_) || utils::path::equal(file_path, pgm_conf_path_)) @@ -170,7 +172,8 @@ void workspace::parse_file(const std::string& file_uri) filter_and_close_dependencies_(found->files_to_close(), found); } } - return; + ws_file_info.config_parsing = true; + return ws_file_info; } // what about removing files??? what if depentands_ points to not existing file? std::vector files_to_parse; @@ -210,8 +213,12 @@ void workspace::parse_file(const std::string& file_uri) const processor_group& grp = get_proc_grp_by_program(f->get_file_name()); f->collect_diags(); + ws_file_info.processor_group_found = &grp != &implicit_proc_grp; if (&grp == &implicit_proc_grp && (int64_t)f->diags().size() > get_config().diag_supress_limit) + { + ws_file_info.diagnostics_suppressed = true; delete_diags(f); + } else diag_suppress_notified_[f->get_file_name()] = false; } @@ -219,6 +226,8 @@ void workspace::parse_file(const std::string& file_uri) // second check after all dependants are there to close all files that used to be dependencies for (auto f : files_to_parse) filter_and_close_dependencies_(f->files_to_close(), f); + + return ws_file_info; } void workspace::refresh_libraries() @@ -232,7 +241,7 @@ void workspace::refresh_libraries() } } -void workspace::did_open_file(const std::string& file_uri) { parse_file(file_uri); } +workspace_file_info workspace::did_open_file(const std::string& file_uri) { return parse_file(file_uri); } void workspace::did_close_file(const std::string& file_uri) { diff --git a/parser_library/src/workspaces/workspace.h b/parser_library/src/workspaces/workspace.h index 367917d53..2bfb52719 100644 --- a/parser_library/src/workspaces/workspace.h +++ b/parser_library/src/workspaces/workspace.h @@ -88,9 +88,9 @@ class workspace : public diagnosable_impl, public parse_lib_provider, public lsp const processor_group& get_proc_grp(const proc_grp_id& proc_grp) const; const processor_group& get_proc_grp_by_program(const std::string& program) const; - void parse_file(const std::string& file_uri); + workspace_file_info parse_file(const std::string& file_uri); void refresh_libraries(); - void did_open_file(const std::string& file_uri); + workspace_file_info did_open_file(const std::string& file_uri); void did_close_file(const std::string& file_uri); void did_change_file(const std::string document_uri, const document_change* changes, size_t ch_size); void did_change_watched_files(const std::string& file_uri); diff --git a/parser_library/test/metrics_test.cpp b/parser_library/test/metrics_test.cpp index 9e146cc8d..c5bf562b3 100644 --- a/parser_library/test/metrics_test.cpp +++ b/parser_library/test/metrics_test.cpp @@ -23,10 +23,10 @@ using namespace hlasm_plugin::parser_library; -class metrics_mock : public performance_metrics_consumer +class metrics_mock : public parsing_metadata_consumer { public: - void consume_performance_metrics(const performance_metrics& metrics) override { metrics_ = metrics; } + void consume_parsing_metadata(const parsing_metadata& metadata) override { metrics_ = metadata.metrics; } performance_metrics metrics_; };