summaryrefslogtreecommitdiff
path: root/cli
diff options
context:
space:
mode:
authorRyan Dahl <ry@tinyclouds.org>2022-01-18 06:58:50 -0500
committerGitHub <noreply@github.com>2022-01-18 06:58:50 -0500
commitce52bfc59c6700e64dbe941485d078ceb9dd2158 (patch)
treea0da203cfeeae9ccf94ed90f3e035dc9596e1b6f /cli
parent39ea4abff4bfa09c4e98e00ad0d4e0e7b78f1187 (diff)
Add LSP benchmark mimicking the one on quick-lint-js (#13365)
Diffstat (limited to 'cli')
-rw-r--r--cli/Cargo.toml6
-rw-r--r--cli/bench/lsp_bench_standalone.rs94
-rw-r--r--cli/bench/testdata/express-router.js663
3 files changed, 763 insertions, 0 deletions
diff --git a/cli/Cargo.toml b/cli/Cargo.toml
index 824cb4da0..1e5c1781e 100644
--- a/cli/Cargo.toml
+++ b/cli/Cargo.toml
@@ -19,6 +19,11 @@ name = "deno_bench"
harness = false
path = "./bench/main.rs"
+[[bench]]
+name = "lsp_bench_standalone"
+harness = false
+path = "./bench/lsp_bench_standalone.rs"
+
[build-dependencies]
deno_broadcast_channel = { version = "0.26.0", path = "../ext/broadcast_channel" }
deno_console = { version = "0.32.0", path = "../ext/console" }
@@ -96,6 +101,7 @@ fwdansi = "=1.1.0"
winapi = { version = "=0.3.9", features = ["knownfolders", "mswsock", "objbase", "shlobj", "tlhelp32", "winbase", "winerror", "winsock2"] }
[dev-dependencies]
+deno_bench_util = { version = "0.26.0", path = "../bench_util" }
flaky_test = "=0.1.0"
os_pipe = "=0.9.2"
pretty_assertions = "=0.7.2"
diff --git a/cli/bench/lsp_bench_standalone.rs b/cli/bench/lsp_bench_standalone.rs
new file mode 100644
index 000000000..4c18a9a9d
--- /dev/null
+++ b/cli/bench/lsp_bench_standalone.rs
@@ -0,0 +1,94 @@
+// Copyright 2018-2022 the Deno authors. All rights reserved. MIT license.
+
+use deno_bench_util::bencher::benchmark_group;
+use deno_bench_util::bencher::benchmark_main;
+use deno_bench_util::bencher::Bencher;
+use deno_core::serde_json;
+use deno_core::serde_json::json;
+use deno_core::serde_json::Value;
+use test_util::lsp::LspClient;
+
+// Intended to match the benchmark in quick-lint-js
+// https://github.com/quick-lint/quick-lint-js/blob/35207e6616267c6c81be63f47ce97ec2452d60df/benchmark/benchmark-lsp/lsp-benchmarks.cpp#L223-L268
+fn incremental_change_wait(bench: &mut Bencher) {
+ let deno_exe = test_util::deno_exe_path();
+ let mut client = LspClient::new(&deno_exe).unwrap();
+
+ static FIXTURE_INIT_JSON: &[u8] =
+ include_bytes!("testdata/initialize_params.json");
+ let params: Value = serde_json::from_slice(FIXTURE_INIT_JSON).unwrap();
+ let (_, maybe_err) = client
+ .write_request::<_, _, Value>("initialize", params)
+ .unwrap();
+ assert!(maybe_err.is_none());
+ client.write_notification("initialized", json!({})).unwrap();
+
+ client
+ .write_notification(
+ "textDocument/didOpen",
+ json!({
+ "textDocument": {
+ "uri": "file:///testdata/express-router.js",
+ "languageId": "javascript",
+ "version": 0,
+ "text": include_str!("testdata/express-router.js")
+ }
+ }),
+ )
+ .unwrap();
+ let (method, _maybe_diag): (String, Option<Value>) =
+ client.read_notification().unwrap();
+ assert_eq!(method, "textDocument/publishDiagnostics");
+
+ let mut document_version: u64 = 0;
+ bench.iter(|| {
+ let text = format!("m{:05}", document_version);
+ client
+ .write_notification(
+ "textDocument/didChange",
+ json!({
+ "textDocument": {
+ "version": document_version,
+ "uri":"file:///testdata/express-router.js"
+ },
+ "contentChanges": [
+ {"text": text, "range":{"start":{"line":506,"character":39},"end":{"line":506,"character":45}}},
+ {"text": text, "range":{"start":{"line":507,"character":8},"end":{"line":507,"character":14}}},
+ {"text": text, "range":{"start":{"line":509,"character":10},"end":{"line":509,"character":16}}}
+ ]
+ })
+ ).unwrap();
+
+ wait_for_deno_lint_diagnostic(document_version, &mut client);
+
+ document_version += 1;
+ })
+}
+
+fn wait_for_deno_lint_diagnostic(
+ document_version: u64,
+ client: &mut LspClient,
+) {
+ loop {
+ let (method, maybe_diag): (String, Option<Value>) =
+ client.read_notification().unwrap();
+ if method == "textDocument/publishDiagnostics" {
+ let d = maybe_diag.unwrap();
+ let msg = d.as_object().unwrap();
+ let version = msg.get("version").unwrap().as_u64().unwrap();
+ if document_version == version {
+ let diagnostics = msg.get("diagnostics").unwrap().as_array().unwrap();
+ let first = &diagnostics[0];
+ let source = first.get("source").unwrap().as_str().unwrap();
+ if source == "deno-lint" {
+ return;
+ }
+ }
+ } else {
+ todo!() // handle_misc_message
+ }
+ }
+}
+
+benchmark_group!(benches, incremental_change_wait);
+benchmark_main!(benches);
diff --git a/cli/bench/testdata/express-router.js b/cli/bench/testdata/express-router.js
new file mode 100644
index 000000000..9b1365aa1
--- /dev/null
+++ b/cli/bench/testdata/express-router.js
@@ -0,0 +1,663 @@
+/*!
+ * express
+ * Copyright(c) 2009-2013 TJ Holowaychuk
+ * Copyright(c) 2013 Roman Shtylman
+ * Copyright(c) 2014-2015 Douglas Christopher Wilson
+ * MIT Licensed
+ */
+
+'use strict';
+
+/**
+ * Module dependencies.
+ * @private
+ */
+
+var Route = require('./route');
+var Layer = require('./layer');
+var methods = require('methods');
+var mixin = require('utils-merge');
+var debug = require('debug')('express:router');
+var deprecate = require('depd')('express');
+var flatten = require('array-flatten');
+var parseUrl = require('parseurl');
+var setPrototypeOf = require('setprototypeof')
+
+/**
+ * Module variables.
+ * @private
+ */
+
+var objectRegExp = /^\[object (\S+)\]$/;
+var slice = Array.prototype.slice;
+var toString = Object.prototype.toString;
+
+/**
+ * Initialize a new `Router` with the given `options`.
+ *
+ * @param {Object} [options]
+ * @return {Router} which is an callable function
+ * @public
+ */
+
+var proto = module.exports = function(options) {
+ var opts = options || {};
+
+ function router(req, res, next) {
+ router.handle(req, res, next);
+ }
+
+ // mixin Router class functions
+ setPrototypeOf(router, proto)
+
+ router.params = {};
+ router._params = [];
+ router.caseSensitive = opts.caseSensitive;
+ router.mergeParams = opts.mergeParams;
+ router.strict = opts.strict;
+ router.stack = [];
+
+ return router;
+};
+
+/**
+ * Map the given param placeholder `name`(s) to the given callback.
+ *
+ * Parameter mapping is used to provide pre-conditions to routes
+ * which use normalized placeholders. For example a _:user_id_ parameter
+ * could automatically load a user's information from the database without
+ * any additional code,
+ *
+ * The callback uses the same signature as middleware, the only difference
+ * being that the value of the placeholder is passed, in this case the _id_
+ * of the user. Once the `next()` function is invoked, just like middleware
+ * it will continue on to execute the route, or subsequent parameter functions.
+ *
+ * Just like in middleware, you must either respond to the request or call next
+ * to avoid stalling the request.
+ *
+ * app.param('user_id', function(req, res, next, id){
+ * User.find(id, function(err, user){
+ * if (err) {
+ * return next(err);
+ * } else if (!user) {
+ * return next(new Error('failed to load user'));
+ * }
+ * req.user = user;
+ * next();
+ * });
+ * });
+ *
+ * @param {String} name
+ * @param {Function} fn
+ * @return {app} for chaining
+ * @public
+ */
+
+proto.param = function param(name, fn) {
+ // param logic
+ if (typeof name === 'function') {
+ deprecate('router.param(fn): Refactor to use path params');
+ this._params.push(name);
+ return;
+ }
+
+ // apply param functions
+ var params = this._params;
+ var len = params.length;
+ var ret;
+
+ if (name[0] === ':') {
+ deprecate('router.param(' + JSON.stringify(name) + ', fn): Use router.param(' + JSON.stringify(name.substr(1)) + ', fn) instead');
+ name = name.substr(1);
+ }
+
+ for (var i = 0; i < len; ++i) {
+ if (ret = params[i](name, fn)) {
+ fn = ret;
+ }
+ }
+
+ // ensure we end up with a
+ // middleware function
+ if ('function' !== typeof fn) {
+ throw new Error('invalid param() call for ' + name + ', got ' + fn);
+ }
+
+ (this.params[name] = this.params[name] || []).push(fn);
+ return this;
+};
+
+/**
+ * Dispatch a req, res into the router.
+ * @private
+ */
+
+proto.handle = function handle(req, res, out) {
+ var self = this;
+
+ debug('dispatching %s %s', req.method, req.url);
+
+ var idx = 0;
+ var protohost = getProtohost(req.url) || ''
+ var removed = '';
+ var slashAdded = false;
+ var paramcalled = {};
+
+ // store options for OPTIONS request
+ // only used if OPTIONS request
+ var options = [];
+
+ // middleware and routes
+ var stack = self.stack;
+
+ // manage inter-router variables
+ var parentParams = req.params;
+ var parentUrl = req.baseUrl || '';
+ var done = restore(out, req, 'baseUrl', 'next', 'params');
+
+ // setup next layer
+ req.next = next;
+
+ // for options requests, respond with a default if nothing else responds
+ if (req.method === 'OPTIONS') {
+ done = wrap(done, function(old, err) {
+ if (err || options.length === 0) return old(err);
+ sendOptionsResponse(res, options, old);
+ });
+ }
+
+ // setup basic req values
+ req.baseUrl = parentUrl;
+ req.originalUrl = req.originalUrl || req.url;
+
+ next();
+
+ function next(err) {
+ var layerError = err === 'route'
+ ? null
+ : err;
+
+ // remove added slash
+ if (slashAdded) {
+ req.url = req.url.substr(1);
+ slashAdded = false;
+ }
+
+ // restore altered req.url
+ if (removed.length !== 0) {
+ req.baseUrl = parentUrl;
+ req.url = protohost + removed + req.url.substr(protohost.length);
+ removed = '';
+ }
+
+ // signal to exit router
+ if (layerError === 'router') {
+ setImmediate(done, null)
+ return
+ }
+
+ // no more matching layers
+ if (idx >= stack.length) {
+ setImmediate(done, layerError);
+ return;
+ }
+
+ // get pathname of request
+ var path = getPathname(req);
+
+ if (path == null) {
+ return done(layerError);
+ }
+
+ // find next matching layer
+ var layer;
+ var match;
+ var route;
+
+ while (match !== true && idx < stack.length) {
+ layer = stack[idx++];
+ match = matchLayer(layer, path);
+ route = layer.route;
+
+ if (typeof match !== 'boolean') {
+ // hold on to layerError
+ layerError = layerError || match;
+ }
+
+ if (match !== true) {
+ continue;
+ }
+
+ if (!route) {
+ // process non-route handlers normally
+ continue;
+ }
+
+ if (layerError) {
+ // routes do not match with a pending error
+ match = false;
+ continue;
+ }
+
+ var method = req.method;
+ var has_method = route._handles_method(method);
+
+ // build up automatic options response
+ if (!has_method && method === 'OPTIONS') {
+ appendMethods(options, route._options());
+ }
+
+ // don't even bother matching route
+ if (!has_method && method !== 'HEAD') {
+ match = false;
+ continue;
+ }
+ }
+
+ // no match
+ if (match !== true) {
+ return done(layerError);
+ }
+
+ // store route for dispatch on change
+ if (route) {
+ req.route = route;
+ }
+
+ // Capture one-time layer values
+ req.params = self.mergeParams
+ ? mergeParams(layer.params, parentParams)
+ : layer.params;
+ var layerPath = layer.path;
+
+ // this should be done for the layer
+ self.process_params(layer, paramcalled, req, res, function (err) {
+ if (err) {
+ return next(layerError || err);
+ }
+
+ if (route) {
+ return layer.handle_request(req, res, next);
+ }
+
+ trim_prefix(layer, layerError, layerPath, path);
+ });
+ }
+
+ function trim_prefix(layer, layerError, layerPath, path) {
+ if (layerPath.length !== 0) {
+ // Validate path breaks on a path separator
+ var c = path[layerPath.length]
+ if (c && c !== '/' && c !== '.') return next(layerError)
+
+ // Trim off the part of the url that matches the route
+ // middleware (.use stuff) needs to have the path stripped
+ debug('trim prefix (%s) from url %s', layerPath, req.url);
+ removed = layerPath;
+ req.url = protohost + req.url.substr(protohost.length + removed.length);
+
+ // Ensure leading slash
+ if (!protohost && req.url[0] !== '/') {
+ req.url = '/' + req.url;
+ slashAdded = true;
+ }
+
+ // Setup base URL (no trailing slash)
+ req.baseUrl = parentUrl + (removed[removed.length - 1] === '/'
+ ? removed.substring(0, removed.length - 1)
+ : removed);
+ }
+
+ debug('%s %s : %s', layer.name, layerPath, req.originalUrl);
+
+ if (layerError) {
+ layer.handle_error(layerError, req, res, next);
+ } else {
+ layer.handle_request(req, res, next);
+ }
+ }
+};
+
+/**
+ * Process any parameters for the layer.
+ * @private
+ */
+
+proto.process_params = function process_params(layer, called, req, res, done) {
+ var params = this.params;
+
+ // captured parameters from the layer, keys and values
+ var keys = layer.keys;
+
+ // fast track
+ if (!keys || keys.length === 0) {
+ return done();
+ }
+
+ var i = 0;
+ var name;
+ var paramIndex = 0;
+ var key;
+ var paramVal;
+ var paramCallbacks;
+ var paramCalled;
+
+ // process params in order
+ // param callbacks can be async
+ function param(err) {
+ if (err) {
+ return done(err);
+ }
+
+ if (i >= keys.length ) {
+ return done();
+ }
+
+ paramIndex = 0;
+ key = keys[i++];
+ name = key.name;
+ paramVal = req.params[name];
+ paramCallbacks = params[name];
+ paramCalled = called[name];
+
+ if (paramVal === undefined || !paramCallbacks) {
+ return param();
+ }
+
+ // param previously called with same value or error occurred
+ if (paramCalled && (paramCalled.match === paramVal
+ || (paramCalled.error && paramCalled.error !== 'route'))) {
+ // restore value
+ req.params[name] = paramCalled.value;
+
+ // next param
+ return param(paramCalled.error);
+ }
+
+ called[name] = paramCalled = {
+ error: null,
+ match: paramVal,
+ value: paramVal
+ };
+
+ paramCallback();
+ }
+
+ // single param callbacks
+ function paramCallback(err) {
+ var fn = paramCallbacks[paramIndex++];
+
+ // store updated value
+ paramCalled.value = req.params[key.name];
+
+ if (err) {
+ // store error
+ paramCalled.error = err;
+ param(err);
+ return;
+ }
+
+ if (!fn) return param();
+
+ try {
+ fn(req, res, paramCallback, paramVal, key.name);
+ } catch (e) {
+ paramCallback(e);
+ }
+ }
+
+ param();
+};
+
+/**
+ * Use the given middleware function, with optional path, defaulting to "/".
+ *
+ * Use (like `.all`) will run for any http METHOD, but it will not add
+ * handlers for those methods so OPTIONS requests will not consider `.use`
+ * functions even if they could respond.
+ *
+ * The other difference is that _route_ path is stripped and not visible
+ * to the handler function. The main effect of this feature is that mounted
+ * handlers can operate without any code changes regardless of the "prefix"
+ * pathname.
+ *
+ * @public
+ */
+
+proto.use = function use(fn) {
+ var offset = 0;
+ var path = '/';
+
+ // default path to '/'
+ // disambiguate router.use([fn])
+ if (typeof fn !== 'function') {
+ var arg = fn;
+
+ while (Array.isArray(arg) && arg.length !== 0) {
+ arg = arg[0];
+ }
+
+ // first arg is the path
+ if (typeof arg !== 'function') {
+ offset = 1;
+ path = fn;
+ }
+ }
+
+ var callbacks = flatten(slice.call(arguments, offset));
+
+ if (callbacks.length === 0) {
+ throw new TypeError('Router.use() requires a middleware function')
+ }
+
+ for (var i = 0; i < callbacks.length; i++) {
+ var fn = callbacks[i];
+
+ if (typeof fn !== 'function') {
+ throw new TypeError('Router.use() requires a middleware function but got a ' + gettype(fn))
+ }
+
+ // add the middleware
+ debug('use %o %s', path, fn.name || '<anonymous>')
+
+ var layer = new Layer(path, {
+ sensitive: this.caseSensitive,
+ strict: false,
+ end: false
+ }, fn);
+
+ layer.route = undefined;
+
+ this.stack.push(layer);
+ }
+
+ return this;
+};
+
+/**
+ * Create a new Route for the given path.
+ *
+ * Each route contains a separate middleware stack and VERB handlers.
+ *
+ * See the Route api documentation for details on adding handlers
+ * and middleware to routes.
+ *
+ * @param {String} path
+ * @return {Route}
+ * @public
+ */
+
+proto.route = function route(path) {
+ var route = new Route(path);
+
+ var layer = new Layer(path, {
+ sensitive: this.caseSensitive,
+ strict: this.strict,
+ end: true
+ }, route.dispatch.bind(route));
+
+ layer.route = route;
+
+ this.stack.push(layer);
+ return route;
+};
+
+// create Router#VERB functions
+methods.concat('all').forEach(function(method){
+ proto[method] = function(path){
+ var route = this.route(path)
+ route[method].apply(route, slice.call(arguments, 1));
+ return this;
+ };
+});
+
+// append methods to a list of methods
+function appendMethods(list, addition) {
+ for (var i = 0; i < addition.length; i++) {
+ var method = addition[i];
+ if (list.indexOf(method) === -1) {
+ list.push(method);
+ }
+ }
+}
+
+// get pathname of request
+function getPathname(req) {
+ try {
+ return parseUrl(req).pathname;
+ } catch (err) {
+ return undefined;
+ }
+}
+
+// Get get protocol + host for a URL
+function getProtohost(url) {
+ if (typeof url !== 'string' || url.length === 0 || url[0] === '/') {
+ return undefined
+ }
+
+ var searchIndex = url.indexOf('?')
+ var pathLength = searchIndex !== -1
+ ? searchIndex
+ : url.length
+ var fqdnIndex = url.substr(0, pathLength).indexOf('://')
+
+ return fqdnIndex !== -1
+ ? url.substr(0, url.indexOf('/', 3 + fqdnIndex))
+ : undefined
+}
+
+// get type for error message
+function gettype(obj) {
+ var type = typeof obj;
+
+ if (type !== 'object') {
+ return type;
+ }
+
+ // inspect [[Class]] for objects
+ return toString.call(obj)
+ .replace(objectRegExp, '$1');
+}
+
+/**
+ * Match path to a layer.
+ *
+ * @param {Layer} layer
+ * @param {string} path
+ * @private
+ */
+
+function matchLayer(layer, path) {
+ try {
+ return layer.match(path);
+ } catch (err) {
+ return err;
+ }
+}
+
+// merge params with parent params
+function mergeParams(params, parent) {
+ if (typeof parent !== 'object' || !parent) {
+ return params;
+ }
+
+ // make copy of parent for base
+ var obj = mixin({}, parent);
+
+ // simple non-numeric merging
+ if (!(0 in params) || !(0 in parent)) {
+ return mixin(obj, params);
+ }
+
+ var i = 0;
+ var o = 0;
+
+ // determine numeric gaps
+ while (i in params) {
+ i++;
+ }
+
+ while (o in parent) {
+ o++;
+ }
+
+ // offset numeric indices in params before merge
+ for (i--; i >= 0; i--) {
+ params[i + o] = params[i];
+
+ // create holes for the merge when necessary
+ if (i < o) {
+ delete params[i];
+ }
+ }
+
+ return mixin(obj, params);
+}
+
+// restore obj props after function
+function restore(fn, obj) {
+ var props = new Array(arguments.length - 2);
+ var vals = new Array(arguments.length - 2);
+
+ for (var i = 0; i < props.length; i++) {
+ props[i] = arguments[i + 2];
+ vals[i] = obj[props[i]];
+ }
+
+ return function () {
+ // restore vals
+ for (var i = 0; i < props.length; i++) {
+ obj[props[i]] = vals[i];
+ }
+
+ return fn.apply(this, arguments);
+ };
+}
+
+// send an OPTIONS response
+function sendOptionsResponse(res, options, next) {
+ try {
+ var body = options.join(',');
+ res.set('Allow', body);
+ res.send(body);
+ } catch (err) {
+ next(err);
+ }
+}
+
+// wrap a function
+function wrap(old, fn) {
+ return function proxy() {
+ var args = new Array(arguments.length + 1);
+
+ args[0] = old;
+ for (var i = 0, len = arguments.length; i < len; i++) {
+ args[i + 1] = arguments[i];
+ }
+
+ fn.apply(this, args);
+ };
+}
+if (false) undeclaredVariable.hasOwnProperty(); // Intentional error to force diagnostics.