From 8a94ab1c81803a6e530ee067b879116525114aba Mon Sep 17 00:00:00 2001 From: akvlad Date: Wed, 31 Jul 2024 17:35:16 +0300 Subject: [PATCH 1/8] init --- pyroscope/json_parsers.js | 39 + pyroscope/proto/querier.proto | 94 +- pyroscope/proto/types/v1/types.proto | 29 +- pyroscope/pyroscope.js | 140 ++- pyroscope/querier_grpc_pb.js | 72 +- pyroscope/querier_pb.js | 1288 +++++++++++++++++++++++++- pyroscope/shared.js | 27 + pyroscope/types/v1/types_pb.js | 588 +++++++++++- 8 files changed, 2215 insertions(+), 62 deletions(-) create mode 100644 pyroscope/json_parsers.js create mode 100644 pyroscope/shared.js diff --git a/pyroscope/json_parsers.js b/pyroscope/json_parsers.js new file mode 100644 index 00000000..37cd6eca --- /dev/null +++ b/pyroscope/json_parsers.js @@ -0,0 +1,39 @@ +const { bufferize } = require('./shared') + +/** + * + * @param req + */ +const series = async (req, payload) => { + let body = await bufferize(payload) + body = JSON.parse(body.toString()) + req.type = 'json' + return { + getStart: () => body.start, + getEnd: () => body.end, + getMatchersList: () => body.matchers, + getLabelNamesList: () => body.labelNames + } +} + +const getProfileStats = async (req, payload) => { + req.type = 'json' + return null +} + +const labelNames = async (req, payload) => { + req.type = 'json' + let body = await bufferize(payload) + body = JSON.parse(body.toString()) + return { + getStart: () => body.start, + getEnd: () => body.end, + getName: () => body.name + } +} + +module.exports = { + series, + getProfileStats, + labelNames +} diff --git a/pyroscope/proto/querier.proto b/pyroscope/proto/querier.proto index f5fc7fc5..78ae0da0 100644 --- a/pyroscope/proto/querier.proto +++ b/pyroscope/proto/querier.proto @@ -16,14 +16,19 @@ service QuerierService { rpc Series(SeriesRequest) returns (SeriesResponse) {} // SelectMergeStacktraces returns matching profiles aggregated in a flamegraph format. It will combine samples from within the same callstack, with each element being grouped by its function name. rpc SelectMergeStacktraces(SelectMergeStacktracesRequest) returns (SelectMergeStacktracesResponse) {} - // SelectMergeSpans returns matching profiles aggregated in a flamegraph format. It will combine samples from within the same callstack, with each element being grouped by its function name. + // SelectMergeSpanProfile returns matching profiles aggregated in a flamegraph format. It will combine samples from within the same callstack, with each element being grouped by its function name. rpc SelectMergeSpanProfile(SelectMergeSpanProfileRequest) returns (SelectMergeSpanProfileResponse) {} // SelectMergeProfile returns matching profiles aggregated in pprof format. It will contain all information stored (so including filenames and line number, if ingested). rpc SelectMergeProfile(SelectMergeProfileRequest) returns (google.v1.Profile) {} // SelectSeries returns a time series for the total sum of the requested profiles. rpc SelectSeries(SelectSeriesRequest) returns (SelectSeriesResponse) {} + // Diff returns a diff of two profiles rpc Diff(DiffRequest) returns (DiffResponse) {} + + // GetProfileStats returns profile stats for the current tenant. + rpc GetProfileStats(types.v1.GetProfileStatsRequest) returns (types.v1.GetProfileStatsResponse) {} + rpc AnalyzeQuery(AnalyzeQueryRequest) returns (AnalyzeQueryResponse) {} } message ProfileTypesRequest { @@ -57,26 +62,48 @@ message SeriesResponse { message SelectMergeStacktracesRequest { string profile_typeID = 1; string label_selector = 2; - int64 start = 3; // milliseconds since epoch - int64 end = 4; // milliseconds since epoch - optional int64 max_nodes = 5; // Limit the nodes returned to only show the node with the max_node's biggest total + // Milliseconds since epoch. + int64 start = 3; + // Milliseconds since epoch. + int64 end = 4; + // Limit the nodes returned to only show the node with the max_node's biggest total + optional int64 max_nodes = 5; + // Profile format specifies the format of profile to be returned. + // If not specified, the profile will be returned in flame graph format. + ProfileFormat format = 6; +} + +enum ProfileFormat { + PROFILE_FORMAT_UNSPECIFIED = 0; + PROFILE_FORMAT_FLAMEGRAPH = 1; + PROFILE_FORMAT_TREE = 2; } message SelectMergeStacktracesResponse { FlameGraph flamegraph = 1; + // Pyroscope tree bytes. + bytes tree = 2; } message SelectMergeSpanProfileRequest { string profile_typeID = 1; string label_selector = 2; repeated string span_selector = 3; - int64 start = 4; // milliseconds since epoch - int64 end = 5; // milliseconds since epoch - optional int64 max_nodes = 6; // Limit the nodes returned to only show the node with the max_node's biggest total + // Milliseconds since epoch. + int64 start = 4; + // Milliseconds since epoch. + int64 end = 5; + // Limit the nodes returned to only show the node with the max_node's biggest total + optional int64 max_nodes = 6; + // Profile format specifies the format of profile to be returned. + // If not specified, the profile will be returned in flame graph format. + ProfileFormat format = 7; } message SelectMergeSpanProfileResponse { FlameGraph flamegraph = 1; + // Pyroscope tree bytes. + bytes tree = 2; } message DiffRequest { @@ -112,22 +139,61 @@ message Level { message SelectMergeProfileRequest { string profile_typeID = 1; string label_selector = 2; - int64 start = 3; // milliseconds since epoch - int64 end = 4; // milliseconds since epoch - optional int64 max_nodes = 5; // Limit the nodes returned to only show the node with the max_node's biggest total - optional types.v1.StackTraceSelector stack_trace_selector = 6; // Only return stack traces matching the selector + // Milliseconds since epoch. + int64 start = 3; + // Milliseconds since epoch. + int64 end = 4; + // Limit the nodes returned to only show the node with the max_node's biggest total + optional int64 max_nodes = 5; + // Select stack traces that match the provided selector. + optional types.v1.StackTraceSelector stack_trace_selector = 6; } message SelectSeriesRequest { string profile_typeID = 1; string label_selector = 2; - int64 start = 3; // milliseconds since epoch - int64 end = 4; // milliseconds since epoch + // Milliseconds since epoch. + int64 start = 3; + // Milliseconds since epoch. + int64 end = 4; repeated string group_by = 5; - double step = 6; // Query resolution step width in seconds + double step = 6; + // Query resolution step width in seconds optional types.v1.TimeSeriesAggregationType aggregation = 7; + // Select stack traces that match the provided selector. + optional types.v1.StackTraceSelector stack_trace_selector = 8; } message SelectSeriesResponse { repeated types.v1.Series series = 1; } + +message AnalyzeQueryRequest { + int64 start = 2; + int64 end = 3; + string query = 4; +} + +message AnalyzeQueryResponse { + repeated QueryScope query_scopes = 1; // detailed view of what the query will require + QueryImpact query_impact = 2; // summary of the query impact / performance +} + +message QueryScope { + string component_type = 1; // a descriptive high level name of the component processing one part of the query (e.g., "short term storage") + uint64 component_count = 2; // how many components of this type will process the query (indicator of read-path replication) + + uint64 block_count = 3; + uint64 series_count = 4; + uint64 profile_count = 5; + uint64 sample_count = 6; + uint64 index_bytes = 7; + uint64 profile_bytes = 8; + uint64 symbol_bytes = 9; +} + +message QueryImpact { + uint64 total_bytes_in_time_range = 2; + uint64 total_queried_series = 3; + bool deduplication_needed = 4; +} diff --git a/pyroscope/proto/types/v1/types.proto b/pyroscope/proto/types/v1/types.proto index 62f8ec94..ced94bf2 100644 --- a/pyroscope/proto/types/v1/types.proto +++ b/pyroscope/proto/types/v1/types.proto @@ -74,11 +74,34 @@ enum TimeSeriesAggregationType { // StackTraceSelector is used for filtering stack traces by locations. message StackTraceSelector { - // Only stack traces that have this stack trace as a prefix will be selected. - // If empty, no stack traces will be selected. Leaf is at stack_trace[0]. - repeated Location stack_trace = 1; + // Stack trace of the call site. Root at call_site[0]. + // Only stack traces having the prefix provided will be selected. + // If empty, the filter is ignored. + repeated Location call_site = 1; + // Stack trace selector for profiles purposed for Go PGO. + // If set, call_site is ignored. + GoPGO go_pgo = 2; } message Location { string name = 1; } + +message GoPGO { + // Specifies the number of leaf locations to keep. + uint32 keep_locations = 1; + // Aggregate callees causes the leaf location line number to be ignored, + // thus aggregating all callee samples (but not callers). + bool aggregate_callees = 2; +} + +message GetProfileStatsRequest {} + +message GetProfileStatsResponse { + // Whether we received any data at any time in the past. + bool data_ingested = 1; + // Milliseconds since epoch. + int64 oldest_profile_time = 2; + // Milliseconds since epoch. + int64 newest_profile_time = 3; +} diff --git a/pyroscope/pyroscope.js b/pyroscope/pyroscope.js index 041382a3..b9cdbcf2 100644 --- a/pyroscope/pyroscope.js +++ b/pyroscope/pyroscope.js @@ -10,7 +10,8 @@ const pprofBin = require('./pprof-bin/pkg/pprof_bin') const { QrynBadRequest } = require('../lib/handlers/errors') const { clusterName } = require('../common') const logger = require('../lib/logger') - +const jsonParsers = require('./json_parsers') +const { bufferize } = require('./shared') const HISTORY_TIMESPAN = 1000 * 60 * 60 * 24 * 7 /** @@ -31,6 +32,20 @@ const parseTypeId = (typeId) => { } } +const wrapResponse = (hndl) => { + return async (req, res) => { + const _res = await hndl(req, res) + if (!_res || !_res.serializeBinary) { + return _res + } + if (req.type === 'json') { + const strRes = JSON.stringify(normalizeProtoResponse(_res.toObject())) + return res.code(200).send(strRes) + } + return res.code(200).send(Buffer.from(_res.serializeBinary())) + } +} + const profileTypesHandler = async (req, res) => { const dist = clusterName ? '_dist' : '' const _res = new messages.ProfileTypesResponse() @@ -57,7 +72,7 @@ WHERE date >= toDate(FROM_UNIXTIME(${Math.floor(fromTimeSec)})) AND date <= toDa pt.setPeriodUnit(periodUnit) return pt })) - return res.code(200).send(Buffer.from(_res.serializeBinary())) + return _res //res.code(200).send(Buffer.from(_res.serializeBinary())) } const labelNames = async (req, res) => { @@ -74,7 +89,7 @@ WHERE date >= toDate(FROM_UNIXTIME(${Math.floor(fromTimeSec)})) AND date <= toDa null, DATABASE_NAME()) const resp = new types.LabelNamesResponse() resp.setNamesList(labelNames.data.data.map(label => label.key)) - return res.code(200).send(Buffer.from(resp.serializeBinary())) + return resp //res.code(200).send(Buffer.from(resp.serializeBinary())) } const labelValues = async (req, res) => { @@ -98,26 +113,12 @@ date >= toDate(FROM_UNIXTIME(${Math.floor(fromTimeSec)})) AND date <= toDate(FROM_UNIXTIME(${Math.floor(toTimeSec)})) FORMAT JSON`, null, DATABASE_NAME()) const resp = new types.LabelValuesResponse() resp.setNamesList(labelValues.data.data.map(label => label.val)) - return res.code(200).send(Buffer.from(resp.serializeBinary())) + return resp //res.code(200).send(Buffer.from(resp.serializeBinary())) } const parser = (MsgClass) => { return async (req, payload) => { - const _body = [] - payload.on('data', data => { - _body.push(data)// += data.toString() - }) - if (payload.isPaused && payload.isPaused()) { - payload.resume() - } - await new Promise(resolve => { - payload.on('end', resolve) - payload.on('close', resolve) - }) - const body = Buffer.concat(_body) - if (body.length === 0) { - return null - } + const body = await bufferize(payload) req._rawBody = body return MsgClass.deserializeBinary(body) } @@ -134,7 +135,7 @@ const labelSelectorQuery = (query, labelSelector) => { if (!labelSelector || !labelSelector.length || labelSelector === '{}') { return query } - const labelSelectorScript = compiler.ParseScript(labelSelector).rootToken + const labelSelectorScript = parseLabelSelector(labelSelector) const labelsConds = [] for (const rule of labelSelectorScript.Children('log_stream_selector_rule')) { const val = JSON.parse(rule.Child('quoted_str').value) @@ -168,12 +169,19 @@ const labelSelectorQuery = (query, labelSelector) => { )) } +const parseLabelSelector = (labelSelector) => { + if (labelSelector.endsWith(',}')) { + labelSelector = labelSelector.slice(0, -2) + '}' + } + return compiler.ParseScript(labelSelector).rootToken +} + const serviceNameSelectorQuery = (labelSelector) => { const empty = Sql.Eq(new Sql.Raw('1'), new Sql.Raw('1')) if (!labelSelector || !labelSelector.length || labelSelector === '{}') { return empty } - const labelSelectorScript = compiler.ParseScript(labelSelector).rootToken + const labelSelectorScript = parseLabelSelector(labelSelector) let conds = null for (const rule of labelSelectorScript.Children('log_stream_selector_rule')) { const label = rule.Child('label').value @@ -491,7 +499,7 @@ const selectSeries = async (req, res) => { const resp = new messages.SelectSeriesResponse() resp.setSeriesList(seriesList) - return res.code(200).send(Buffer.from(resp.serializeBinary())) + return resp //res.code(200).send(Buffer.from(resp.serializeBinary())) } const selectMergeProfile = async (req, res) => { @@ -607,13 +615,30 @@ const series = async (req, res) => { ) promises.push(clickhouse.rawRequest(labelsReq.toString() + ' FORMAT JSON', null, DATABASE_NAME())) } + if ((_req.getMatchersList() || []).length === 0) { + const labelsReq = (new Sql.Select()) + .select( + ['tags', 'tags'], + ['type_id', 'type_id'], + ['sample_types_units', '_sample_types_units']) + .from([`${DATABASE_NAME()}.profiles_series${dist}`, 'p']) + .join('p.sample_types_units', 'array') + .where( + Sql.And( + Sql.Gte('date', new Sql.Raw(`toDate(FROM_UNIXTIME(${Math.floor(fromTimeSec)}))`)), + Sql.Lte('date', new Sql.Raw(`toDate(FROM_UNIXTIME(${Math.floor(toTimeSec)}))`)), + ) + ) + promises.push(clickhouse.rawRequest(labelsReq.toString() + ' FORMAT JSON', null, DATABASE_NAME())) + } const resp = await Promise.all(promises) const response = new messages.SeriesResponse() const labelsSet = [] + const filterLabelNames = _req.getLabelNamesList() || null resp.forEach(_res => { for (const row of _res.data.data) { const labels = new types.Labels() - const _labels = [] + let _labels = [] for (const tag of row.tags) { const pair = new types.LabelPair() pair.setName(tag[0]) @@ -636,12 +661,40 @@ const series = async (req, res) => { _pair('__profile_type__', `${typeId[0]}:${row._sample_types_units[0]}:${row._sample_types_units[1]}:${typeId[1]}:${typeId[2]}`) ) - labels.setLabelsList(_labels) - labelsSet.push(labels) + if (filterLabelNames && filterLabelNames.length) { + _labels = _labels.filter((l) => filterLabelNames.includes(l.getName())) + } + if (_labels.length > 0) { + labels.setLabelsList(_labels) + labelsSet.push(labels) + } } }) response.setLabelsSetList(labelsSet) - return res.code(200).send(Buffer.from(response.serializeBinary())) + return response //res.code(200).send(Buffer.from(response.serializeBinary())) +} + +/** + * + * @param proto {Object} + */ +const normalizeProtoResponse = (proto) => { + if (typeof proto !== 'object') { + return proto + } + return Object.fromEntries(Object.entries(proto).map((e) => { + let name = e[0] + if (name.endsWith('List')) { + name = name.slice(0, -4) + } + if (Array.isArray(e[1])) { + return [name, e[1].map(normalizeProtoResponse)] + } + if (typeof e[1] === 'object') { + return [name, normalizeProtoResponse(e[1])] + } + return [name, e[1]] + })) } /** @@ -722,6 +775,30 @@ const specialMatchersQuery = (matchers) => { return new Sql.And(...clauses) } +const getProfileStats = async (req, res) => { + const sql = ` +with non_empty as (select any(1) as non_empty from profiles limit 1), + min_date as (select min(date) as min_date, max(date) as max_date from profiles_series), + min_time as ( + select intDiv(min(timestamp_ns), 1000000) as min_time, + intDiv(max(timestamp_ns), 1000000) as max_time + from profiles + where timestamp_ns < toUnixTimestamp((select any (min_date) from min_date) + INTERVAL '1 day') * 1000000000 OR + timestamp_ns >= toUnixTimestamp((select any(max_date) from min_date)) * 1000000000 + ) +select + (select any(non_empty) from non_empty) as non_empty, + (select any(min_time) from min_time) as min_time, + (select any(max_time) from min_time) as max_time +` + const sqlRes = await clickhouse.rawRequest(sql + ' FORMAT JSON', null, DATABASE_NAME()) + const response = new types.GetProfileStatsResponse() + response.setDataIngested(!!sqlRes.data.data[0].non_empty) + response.setOldestProfileTime(sqlRes.data.data[0].min_time) + response.setNewestProfileTime(sqlRes.data.data[0].max_time) + return response//res.code(200).send(Buffer.from(response.serializeBinary())) +} + module.exports.init = (fastify) => { const fns = { profileTypes: profileTypesHandler, @@ -730,12 +807,19 @@ module.exports.init = (fastify) => { selectMergeStacktraces: selectMergeStacktraces, selectSeries: selectSeries, selectMergeProfile: selectMergeProfile, - series: series + series: series, + getProfileStats: getProfileStats + } + const parsers = { + series: jsonParsers.series, + getProfileStats: jsonParsers.getProfileStats, + labelNames: jsonParsers.labelNames } for (const name of Object.keys(fns)) { fastify.post(services.QuerierServiceService[name].path, (req, res) => { - return fns[name](req, res) + return wrapResponse(fns[name])(req, res) }, { + 'application/json': parsers[name], '*': parser(services.QuerierServiceService[name].requestType) }) } diff --git a/pyroscope/querier_grpc_pb.js b/pyroscope/querier_grpc_pb.js index f4660485..4f473c34 100644 --- a/pyroscope/querier_grpc_pb.js +++ b/pyroscope/querier_grpc_pb.js @@ -17,6 +17,28 @@ function deserialize_google_v1_Profile(buffer_arg) { return google_v1_profile_pb.Profile.deserializeBinary(new Uint8Array(buffer_arg)); } +function serialize_querier_v1_AnalyzeQueryRequest(arg) { + if (!(arg instanceof querier_pb.AnalyzeQueryRequest)) { + throw new Error('Expected argument of type querier.v1.AnalyzeQueryRequest'); + } + return Buffer.from(arg.serializeBinary()); +} + +function deserialize_querier_v1_AnalyzeQueryRequest(buffer_arg) { + return querier_pb.AnalyzeQueryRequest.deserializeBinary(new Uint8Array(buffer_arg)); +} + +function serialize_querier_v1_AnalyzeQueryResponse(arg) { + if (!(arg instanceof querier_pb.AnalyzeQueryResponse)) { + throw new Error('Expected argument of type querier.v1.AnalyzeQueryResponse'); + } + return Buffer.from(arg.serializeBinary()); +} + +function deserialize_querier_v1_AnalyzeQueryResponse(buffer_arg) { + return querier_pb.AnalyzeQueryResponse.deserializeBinary(new Uint8Array(buffer_arg)); +} + function serialize_querier_v1_DiffRequest(arg) { if (!(arg instanceof querier_pb.DiffRequest)) { throw new Error('Expected argument of type querier.v1.DiffRequest'); @@ -160,6 +182,28 @@ function deserialize_querier_v1_SeriesResponse(buffer_arg) { return querier_pb.SeriesResponse.deserializeBinary(new Uint8Array(buffer_arg)); } +function serialize_types_v1_GetProfileStatsRequest(arg) { + if (!(arg instanceof types_v1_types_pb.GetProfileStatsRequest)) { + throw new Error('Expected argument of type types.v1.GetProfileStatsRequest'); + } + return Buffer.from(arg.serializeBinary()); +} + +function deserialize_types_v1_GetProfileStatsRequest(buffer_arg) { + return types_v1_types_pb.GetProfileStatsRequest.deserializeBinary(new Uint8Array(buffer_arg)); +} + +function serialize_types_v1_GetProfileStatsResponse(arg) { + if (!(arg instanceof types_v1_types_pb.GetProfileStatsResponse)) { + throw new Error('Expected argument of type types.v1.GetProfileStatsResponse'); + } + return Buffer.from(arg.serializeBinary()); +} + +function deserialize_types_v1_GetProfileStatsResponse(buffer_arg) { + return types_v1_types_pb.GetProfileStatsResponse.deserializeBinary(new Uint8Array(buffer_arg)); +} + function serialize_types_v1_LabelNamesRequest(arg) { if (!(arg instanceof types_v1_types_pb.LabelNamesRequest)) { throw new Error('Expected argument of type types.v1.LabelNamesRequest'); @@ -266,7 +310,7 @@ selectMergeStacktraces: { responseSerialize: serialize_querier_v1_SelectMergeStacktracesResponse, responseDeserialize: deserialize_querier_v1_SelectMergeStacktracesResponse, }, - // SelectMergeSpans returns matching profiles aggregated in a flamegraph format. It will combine samples from within the same callstack, with each element being grouped by its function name. + // SelectMergeSpanProfile returns matching profiles aggregated in a flamegraph format. It will combine samples from within the same callstack, with each element being grouped by its function name. selectMergeSpanProfile: { path: '/querier.v1.QuerierService/SelectMergeSpanProfile', requestStream: false, @@ -302,7 +346,8 @@ selectSeries: { responseSerialize: serialize_querier_v1_SelectSeriesResponse, responseDeserialize: deserialize_querier_v1_SelectSeriesResponse, }, - diff: { + // Diff returns a diff of two profiles +diff: { path: '/querier.v1.QuerierService/Diff', requestStream: false, responseStream: false, @@ -313,6 +358,29 @@ selectSeries: { responseSerialize: serialize_querier_v1_DiffResponse, responseDeserialize: deserialize_querier_v1_DiffResponse, }, + // GetProfileStats returns profile stats for the current tenant. +getProfileStats: { + path: '/querier.v1.QuerierService/GetProfileStats', + requestStream: false, + responseStream: false, + requestType: types_v1_types_pb.GetProfileStatsRequest, + responseType: types_v1_types_pb.GetProfileStatsResponse, + requestSerialize: serialize_types_v1_GetProfileStatsRequest, + requestDeserialize: deserialize_types_v1_GetProfileStatsRequest, + responseSerialize: serialize_types_v1_GetProfileStatsResponse, + responseDeserialize: deserialize_types_v1_GetProfileStatsResponse, + }, + analyzeQuery: { + path: '/querier.v1.QuerierService/AnalyzeQuery', + requestStream: false, + responseStream: false, + requestType: querier_pb.AnalyzeQueryRequest, + responseType: querier_pb.AnalyzeQueryResponse, + requestSerialize: serialize_querier_v1_AnalyzeQueryRequest, + requestDeserialize: deserialize_querier_v1_AnalyzeQueryRequest, + responseSerialize: serialize_querier_v1_AnalyzeQueryResponse, + responseDeserialize: deserialize_querier_v1_AnalyzeQueryResponse, + }, }; exports.QuerierServiceClient = grpc.makeGenericClientConstructor(QuerierServiceService); diff --git a/pyroscope/querier_pb.js b/pyroscope/querier_pb.js index 8988dda9..82334400 100644 --- a/pyroscope/querier_pb.js +++ b/pyroscope/querier_pb.js @@ -25,13 +25,18 @@ var google_v1_profile_pb = require('./google/v1/profile_pb.js'); goog.object.extend(proto, google_v1_profile_pb); var types_v1_types_pb = require('./types/v1/types_pb.js'); goog.object.extend(proto, types_v1_types_pb); +goog.exportSymbol('proto.querier.v1.AnalyzeQueryRequest', null, global); +goog.exportSymbol('proto.querier.v1.AnalyzeQueryResponse', null, global); goog.exportSymbol('proto.querier.v1.DiffRequest', null, global); goog.exportSymbol('proto.querier.v1.DiffResponse', null, global); goog.exportSymbol('proto.querier.v1.FlameGraph', null, global); goog.exportSymbol('proto.querier.v1.FlameGraphDiff', null, global); goog.exportSymbol('proto.querier.v1.Level', null, global); +goog.exportSymbol('proto.querier.v1.ProfileFormat', null, global); goog.exportSymbol('proto.querier.v1.ProfileTypesRequest', null, global); goog.exportSymbol('proto.querier.v1.ProfileTypesResponse', null, global); +goog.exportSymbol('proto.querier.v1.QueryImpact', null, global); +goog.exportSymbol('proto.querier.v1.QueryScope', null, global); goog.exportSymbol('proto.querier.v1.SelectMergeProfileRequest', null, global); goog.exportSymbol('proto.querier.v1.SelectMergeSpanProfileRequest', null, global); goog.exportSymbol('proto.querier.v1.SelectMergeSpanProfileResponse', null, global); @@ -377,6 +382,90 @@ if (goog.DEBUG && !COMPILED) { */ proto.querier.v1.SelectSeriesResponse.displayName = 'proto.querier.v1.SelectSeriesResponse'; } +/** + * Generated by JsPbCodeGenerator. + * @param {Array=} opt_data Optional initial data array, typically from a + * server response, or constructed directly in Javascript. The array is used + * in place and becomes part of the constructed object. It is not cloned. + * If no data is provided, the constructed object will be empty, but still + * valid. + * @extends {jspb.Message} + * @constructor + */ +proto.querier.v1.AnalyzeQueryRequest = function(opt_data) { + jspb.Message.initialize(this, opt_data, 0, -1, null, null); +}; +goog.inherits(proto.querier.v1.AnalyzeQueryRequest, jspb.Message); +if (goog.DEBUG && !COMPILED) { + /** + * @public + * @override + */ + proto.querier.v1.AnalyzeQueryRequest.displayName = 'proto.querier.v1.AnalyzeQueryRequest'; +} +/** + * Generated by JsPbCodeGenerator. + * @param {Array=} opt_data Optional initial data array, typically from a + * server response, or constructed directly in Javascript. The array is used + * in place and becomes part of the constructed object. It is not cloned. + * If no data is provided, the constructed object will be empty, but still + * valid. + * @extends {jspb.Message} + * @constructor + */ +proto.querier.v1.AnalyzeQueryResponse = function(opt_data) { + jspb.Message.initialize(this, opt_data, 0, -1, proto.querier.v1.AnalyzeQueryResponse.repeatedFields_, null); +}; +goog.inherits(proto.querier.v1.AnalyzeQueryResponse, jspb.Message); +if (goog.DEBUG && !COMPILED) { + /** + * @public + * @override + */ + proto.querier.v1.AnalyzeQueryResponse.displayName = 'proto.querier.v1.AnalyzeQueryResponse'; +} +/** + * Generated by JsPbCodeGenerator. + * @param {Array=} opt_data Optional initial data array, typically from a + * server response, or constructed directly in Javascript. The array is used + * in place and becomes part of the constructed object. It is not cloned. + * If no data is provided, the constructed object will be empty, but still + * valid. + * @extends {jspb.Message} + * @constructor + */ +proto.querier.v1.QueryScope = function(opt_data) { + jspb.Message.initialize(this, opt_data, 0, -1, null, null); +}; +goog.inherits(proto.querier.v1.QueryScope, jspb.Message); +if (goog.DEBUG && !COMPILED) { + /** + * @public + * @override + */ + proto.querier.v1.QueryScope.displayName = 'proto.querier.v1.QueryScope'; +} +/** + * Generated by JsPbCodeGenerator. + * @param {Array=} opt_data Optional initial data array, typically from a + * server response, or constructed directly in Javascript. The array is used + * in place and becomes part of the constructed object. It is not cloned. + * If no data is provided, the constructed object will be empty, but still + * valid. + * @extends {jspb.Message} + * @constructor + */ +proto.querier.v1.QueryImpact = function(opt_data) { + jspb.Message.initialize(this, opt_data, 0, -1, null, null); +}; +goog.inherits(proto.querier.v1.QueryImpact, jspb.Message); +if (goog.DEBUG && !COMPILED) { + /** + * @public + * @override + */ + proto.querier.v1.QueryImpact.displayName = 'proto.querier.v1.QueryImpact'; +} @@ -1158,7 +1247,8 @@ proto.querier.v1.SelectMergeStacktracesRequest.toObject = function(includeInstan labelSelector: jspb.Message.getFieldWithDefault(msg, 2, ""), start: jspb.Message.getFieldWithDefault(msg, 3, 0), end: jspb.Message.getFieldWithDefault(msg, 4, 0), - maxNodes: jspb.Message.getFieldWithDefault(msg, 5, 0) + maxNodes: jspb.Message.getFieldWithDefault(msg, 5, 0), + format: jspb.Message.getFieldWithDefault(msg, 6, 0) }; if (includeInstance) { @@ -1215,6 +1305,10 @@ proto.querier.v1.SelectMergeStacktracesRequest.deserializeBinaryFromReader = fun var value = /** @type {number} */ (reader.readInt64()); msg.setMaxNodes(value); break; + case 6: + var value = /** @type {!proto.querier.v1.ProfileFormat} */ (reader.readEnum()); + msg.setFormat(value); + break; default: reader.skipField(); break; @@ -1279,6 +1373,13 @@ proto.querier.v1.SelectMergeStacktracesRequest.serializeBinaryToWriter = functio f ); } + f = message.getFormat(); + if (f !== 0.0) { + writer.writeEnum( + 6, + f + ); + } }; @@ -1390,6 +1491,24 @@ proto.querier.v1.SelectMergeStacktracesRequest.prototype.hasMaxNodes = function( }; +/** + * optional ProfileFormat format = 6; + * @return {!proto.querier.v1.ProfileFormat} + */ +proto.querier.v1.SelectMergeStacktracesRequest.prototype.getFormat = function() { + return /** @type {!proto.querier.v1.ProfileFormat} */ (jspb.Message.getFieldWithDefault(this, 6, 0)); +}; + + +/** + * @param {!proto.querier.v1.ProfileFormat} value + * @return {!proto.querier.v1.SelectMergeStacktracesRequest} returns this + */ +proto.querier.v1.SelectMergeStacktracesRequest.prototype.setFormat = function(value) { + return jspb.Message.setProto3EnumField(this, 6, value); +}; + + @@ -1422,7 +1541,8 @@ proto.querier.v1.SelectMergeStacktracesResponse.prototype.toObject = function(op */ proto.querier.v1.SelectMergeStacktracesResponse.toObject = function(includeInstance, msg) { var f, obj = { - flamegraph: (f = msg.getFlamegraph()) && proto.querier.v1.FlameGraph.toObject(includeInstance, f) + flamegraph: (f = msg.getFlamegraph()) && proto.querier.v1.FlameGraph.toObject(includeInstance, f), + tree: msg.getTree_asB64() }; if (includeInstance) { @@ -1464,6 +1584,10 @@ proto.querier.v1.SelectMergeStacktracesResponse.deserializeBinaryFromReader = fu reader.readMessage(value,proto.querier.v1.FlameGraph.deserializeBinaryFromReader); msg.setFlamegraph(value); break; + case 2: + var value = /** @type {!Uint8Array} */ (reader.readBytes()); + msg.setTree(value); + break; default: reader.skipField(); break; @@ -1501,6 +1625,13 @@ proto.querier.v1.SelectMergeStacktracesResponse.serializeBinaryToWriter = functi proto.querier.v1.FlameGraph.serializeBinaryToWriter ); } + f = message.getTree_asU8(); + if (f.length > 0) { + writer.writeBytes( + 2, + f + ); + } }; @@ -1541,6 +1672,48 @@ proto.querier.v1.SelectMergeStacktracesResponse.prototype.hasFlamegraph = functi }; +/** + * optional bytes tree = 2; + * @return {!(string|Uint8Array)} + */ +proto.querier.v1.SelectMergeStacktracesResponse.prototype.getTree = function() { + return /** @type {!(string|Uint8Array)} */ (jspb.Message.getFieldWithDefault(this, 2, "")); +}; + + +/** + * optional bytes tree = 2; + * This is a type-conversion wrapper around `getTree()` + * @return {string} + */ +proto.querier.v1.SelectMergeStacktracesResponse.prototype.getTree_asB64 = function() { + return /** @type {string} */ (jspb.Message.bytesAsB64( + this.getTree())); +}; + + +/** + * optional bytes tree = 2; + * Note that Uint8Array is not supported on all browsers. + * @see http://caniuse.com/Uint8Array + * This is a type-conversion wrapper around `getTree()` + * @return {!Uint8Array} + */ +proto.querier.v1.SelectMergeStacktracesResponse.prototype.getTree_asU8 = function() { + return /** @type {!Uint8Array} */ (jspb.Message.bytesAsU8( + this.getTree())); +}; + + +/** + * @param {!(string|Uint8Array)} value + * @return {!proto.querier.v1.SelectMergeStacktracesResponse} returns this + */ +proto.querier.v1.SelectMergeStacktracesResponse.prototype.setTree = function(value) { + return jspb.Message.setProto3BytesField(this, 2, value); +}; + + /** * List of repeated fields within this message type. @@ -1585,7 +1758,8 @@ proto.querier.v1.SelectMergeSpanProfileRequest.toObject = function(includeInstan spanSelectorList: (f = jspb.Message.getRepeatedField(msg, 3)) == null ? undefined : f, start: jspb.Message.getFieldWithDefault(msg, 4, 0), end: jspb.Message.getFieldWithDefault(msg, 5, 0), - maxNodes: jspb.Message.getFieldWithDefault(msg, 6, 0) + maxNodes: jspb.Message.getFieldWithDefault(msg, 6, 0), + format: jspb.Message.getFieldWithDefault(msg, 7, 0) }; if (includeInstance) { @@ -1646,6 +1820,10 @@ proto.querier.v1.SelectMergeSpanProfileRequest.deserializeBinaryFromReader = fun var value = /** @type {number} */ (reader.readInt64()); msg.setMaxNodes(value); break; + case 7: + var value = /** @type {!proto.querier.v1.ProfileFormat} */ (reader.readEnum()); + msg.setFormat(value); + break; default: reader.skipField(); break; @@ -1717,6 +1895,13 @@ proto.querier.v1.SelectMergeSpanProfileRequest.serializeBinaryToWriter = functio f ); } + f = message.getFormat(); + if (f !== 0.0) { + writer.writeEnum( + 7, + f + ); + } }; @@ -1865,6 +2050,24 @@ proto.querier.v1.SelectMergeSpanProfileRequest.prototype.hasMaxNodes = function( }; +/** + * optional ProfileFormat format = 7; + * @return {!proto.querier.v1.ProfileFormat} + */ +proto.querier.v1.SelectMergeSpanProfileRequest.prototype.getFormat = function() { + return /** @type {!proto.querier.v1.ProfileFormat} */ (jspb.Message.getFieldWithDefault(this, 7, 0)); +}; + + +/** + * @param {!proto.querier.v1.ProfileFormat} value + * @return {!proto.querier.v1.SelectMergeSpanProfileRequest} returns this + */ +proto.querier.v1.SelectMergeSpanProfileRequest.prototype.setFormat = function(value) { + return jspb.Message.setProto3EnumField(this, 7, value); +}; + + @@ -1897,7 +2100,8 @@ proto.querier.v1.SelectMergeSpanProfileResponse.prototype.toObject = function(op */ proto.querier.v1.SelectMergeSpanProfileResponse.toObject = function(includeInstance, msg) { var f, obj = { - flamegraph: (f = msg.getFlamegraph()) && proto.querier.v1.FlameGraph.toObject(includeInstance, f) + flamegraph: (f = msg.getFlamegraph()) && proto.querier.v1.FlameGraph.toObject(includeInstance, f), + tree: msg.getTree_asB64() }; if (includeInstance) { @@ -1939,6 +2143,10 @@ proto.querier.v1.SelectMergeSpanProfileResponse.deserializeBinaryFromReader = fu reader.readMessage(value,proto.querier.v1.FlameGraph.deserializeBinaryFromReader); msg.setFlamegraph(value); break; + case 2: + var value = /** @type {!Uint8Array} */ (reader.readBytes()); + msg.setTree(value); + break; default: reader.skipField(); break; @@ -1976,6 +2184,13 @@ proto.querier.v1.SelectMergeSpanProfileResponse.serializeBinaryToWriter = functi proto.querier.v1.FlameGraph.serializeBinaryToWriter ); } + f = message.getTree_asU8(); + if (f.length > 0) { + writer.writeBytes( + 2, + f + ); + } }; @@ -2016,6 +2231,48 @@ proto.querier.v1.SelectMergeSpanProfileResponse.prototype.hasFlamegraph = functi }; +/** + * optional bytes tree = 2; + * @return {!(string|Uint8Array)} + */ +proto.querier.v1.SelectMergeSpanProfileResponse.prototype.getTree = function() { + return /** @type {!(string|Uint8Array)} */ (jspb.Message.getFieldWithDefault(this, 2, "")); +}; + + +/** + * optional bytes tree = 2; + * This is a type-conversion wrapper around `getTree()` + * @return {string} + */ +proto.querier.v1.SelectMergeSpanProfileResponse.prototype.getTree_asB64 = function() { + return /** @type {string} */ (jspb.Message.bytesAsB64( + this.getTree())); +}; + + +/** + * optional bytes tree = 2; + * Note that Uint8Array is not supported on all browsers. + * @see http://caniuse.com/Uint8Array + * This is a type-conversion wrapper around `getTree()` + * @return {!Uint8Array} + */ +proto.querier.v1.SelectMergeSpanProfileResponse.prototype.getTree_asU8 = function() { + return /** @type {!Uint8Array} */ (jspb.Message.bytesAsU8( + this.getTree())); +}; + + +/** + * @param {!(string|Uint8Array)} value + * @return {!proto.querier.v1.SelectMergeSpanProfileResponse} returns this + */ +proto.querier.v1.SelectMergeSpanProfileResponse.prototype.setTree = function(value) { + return jspb.Message.setProto3BytesField(this, 2, value); +}; + + @@ -3489,7 +3746,8 @@ proto.querier.v1.SelectSeriesRequest.toObject = function(includeInstance, msg) { end: jspb.Message.getFieldWithDefault(msg, 4, 0), groupByList: (f = jspb.Message.getRepeatedField(msg, 5)) == null ? undefined : f, step: jspb.Message.getFloatingPointFieldWithDefault(msg, 6, 0.0), - aggregation: jspb.Message.getFieldWithDefault(msg, 7, 0) + aggregation: jspb.Message.getFieldWithDefault(msg, 7, 0), + stackTraceSelector: (f = msg.getStackTraceSelector()) && types_v1_types_pb.StackTraceSelector.toObject(includeInstance, f) }; if (includeInstance) { @@ -3554,6 +3812,11 @@ proto.querier.v1.SelectSeriesRequest.deserializeBinaryFromReader = function(msg, var value = /** @type {!proto.types.v1.TimeSeriesAggregationType} */ (reader.readEnum()); msg.setAggregation(value); break; + case 8: + var value = new types_v1_types_pb.StackTraceSelector; + reader.readMessage(value,types_v1_types_pb.StackTraceSelector.deserializeBinaryFromReader); + msg.setStackTraceSelector(value); + break; default: reader.skipField(); break; @@ -3632,6 +3895,14 @@ proto.querier.v1.SelectSeriesRequest.serializeBinaryToWriter = function(message, f ); } + f = message.getStackTraceSelector(); + if (f != null) { + writer.writeMessage( + 8, + f, + types_v1_types_pb.StackTraceSelector.serializeBinaryToWriter + ); + } }; @@ -3798,6 +4069,43 @@ proto.querier.v1.SelectSeriesRequest.prototype.hasAggregation = function() { }; +/** + * optional types.v1.StackTraceSelector stack_trace_selector = 8; + * @return {?proto.types.v1.StackTraceSelector} + */ +proto.querier.v1.SelectSeriesRequest.prototype.getStackTraceSelector = function() { + return /** @type{?proto.types.v1.StackTraceSelector} */ ( + jspb.Message.getWrapperField(this, types_v1_types_pb.StackTraceSelector, 8)); +}; + + +/** + * @param {?proto.types.v1.StackTraceSelector|undefined} value + * @return {!proto.querier.v1.SelectSeriesRequest} returns this +*/ +proto.querier.v1.SelectSeriesRequest.prototype.setStackTraceSelector = function(value) { + return jspb.Message.setWrapperField(this, 8, value); +}; + + +/** + * Clears the message field making it undefined. + * @return {!proto.querier.v1.SelectSeriesRequest} returns this + */ +proto.querier.v1.SelectSeriesRequest.prototype.clearStackTraceSelector = function() { + return this.setStackTraceSelector(undefined); +}; + + +/** + * Returns whether this field is set. + * @return {boolean} + */ +proto.querier.v1.SelectSeriesRequest.prototype.hasStackTraceSelector = function() { + return jspb.Message.getField(this, 8) != null; +}; + + /** * List of repeated fields within this message type. @@ -3958,4 +4266,974 @@ proto.querier.v1.SelectSeriesResponse.prototype.clearSeriesList = function() { }; + + + +if (jspb.Message.GENERATE_TO_OBJECT) { +/** + * Creates an object representation of this proto. + * Field names that are reserved in JavaScript and will be renamed to pb_name. + * Optional fields that are not set will be set to undefined. + * To access a reserved field use, foo.pb_, eg, foo.pb_default. + * For the list of reserved names please see: + * net/proto2/compiler/js/internal/generator.cc#kKeyword. + * @param {boolean=} opt_includeInstance Deprecated. whether to include the + * JSPB instance for transitional soy proto support: + * http://goto/soy-param-migration + * @return {!Object} + */ +proto.querier.v1.AnalyzeQueryRequest.prototype.toObject = function(opt_includeInstance) { + return proto.querier.v1.AnalyzeQueryRequest.toObject(opt_includeInstance, this); +}; + + +/** + * Static version of the {@see toObject} method. + * @param {boolean|undefined} includeInstance Deprecated. Whether to include + * the JSPB instance for transitional soy proto support: + * http://goto/soy-param-migration + * @param {!proto.querier.v1.AnalyzeQueryRequest} msg The msg instance to transform. + * @return {!Object} + * @suppress {unusedLocalVariables} f is only used for nested messages + */ +proto.querier.v1.AnalyzeQueryRequest.toObject = function(includeInstance, msg) { + var f, obj = { + start: jspb.Message.getFieldWithDefault(msg, 2, 0), + end: jspb.Message.getFieldWithDefault(msg, 3, 0), + query: jspb.Message.getFieldWithDefault(msg, 4, "") + }; + + if (includeInstance) { + obj.$jspbMessageInstance = msg; + } + return obj; +}; +} + + +/** + * Deserializes binary data (in protobuf wire format). + * @param {jspb.ByteSource} bytes The bytes to deserialize. + * @return {!proto.querier.v1.AnalyzeQueryRequest} + */ +proto.querier.v1.AnalyzeQueryRequest.deserializeBinary = function(bytes) { + var reader = new jspb.BinaryReader(bytes); + var msg = new proto.querier.v1.AnalyzeQueryRequest; + return proto.querier.v1.AnalyzeQueryRequest.deserializeBinaryFromReader(msg, reader); +}; + + +/** + * Deserializes binary data (in protobuf wire format) from the + * given reader into the given message object. + * @param {!proto.querier.v1.AnalyzeQueryRequest} msg The message object to deserialize into. + * @param {!jspb.BinaryReader} reader The BinaryReader to use. + * @return {!proto.querier.v1.AnalyzeQueryRequest} + */ +proto.querier.v1.AnalyzeQueryRequest.deserializeBinaryFromReader = function(msg, reader) { + while (reader.nextField()) { + if (reader.isEndGroup()) { + break; + } + var field = reader.getFieldNumber(); + switch (field) { + case 2: + var value = /** @type {number} */ (reader.readInt64()); + msg.setStart(value); + break; + case 3: + var value = /** @type {number} */ (reader.readInt64()); + msg.setEnd(value); + break; + case 4: + var value = /** @type {string} */ (reader.readString()); + msg.setQuery(value); + break; + default: + reader.skipField(); + break; + } + } + return msg; +}; + + +/** + * Serializes the message to binary data (in protobuf wire format). + * @return {!Uint8Array} + */ +proto.querier.v1.AnalyzeQueryRequest.prototype.serializeBinary = function() { + var writer = new jspb.BinaryWriter(); + proto.querier.v1.AnalyzeQueryRequest.serializeBinaryToWriter(this, writer); + return writer.getResultBuffer(); +}; + + +/** + * Serializes the given message to binary data (in protobuf wire + * format), writing to the given BinaryWriter. + * @param {!proto.querier.v1.AnalyzeQueryRequest} message + * @param {!jspb.BinaryWriter} writer + * @suppress {unusedLocalVariables} f is only used for nested messages + */ +proto.querier.v1.AnalyzeQueryRequest.serializeBinaryToWriter = function(message, writer) { + var f = undefined; + f = message.getStart(); + if (f !== 0) { + writer.writeInt64( + 2, + f + ); + } + f = message.getEnd(); + if (f !== 0) { + writer.writeInt64( + 3, + f + ); + } + f = message.getQuery(); + if (f.length > 0) { + writer.writeString( + 4, + f + ); + } +}; + + +/** + * optional int64 start = 2; + * @return {number} + */ +proto.querier.v1.AnalyzeQueryRequest.prototype.getStart = function() { + return /** @type {number} */ (jspb.Message.getFieldWithDefault(this, 2, 0)); +}; + + +/** + * @param {number} value + * @return {!proto.querier.v1.AnalyzeQueryRequest} returns this + */ +proto.querier.v1.AnalyzeQueryRequest.prototype.setStart = function(value) { + return jspb.Message.setProto3IntField(this, 2, value); +}; + + +/** + * optional int64 end = 3; + * @return {number} + */ +proto.querier.v1.AnalyzeQueryRequest.prototype.getEnd = function() { + return /** @type {number} */ (jspb.Message.getFieldWithDefault(this, 3, 0)); +}; + + +/** + * @param {number} value + * @return {!proto.querier.v1.AnalyzeQueryRequest} returns this + */ +proto.querier.v1.AnalyzeQueryRequest.prototype.setEnd = function(value) { + return jspb.Message.setProto3IntField(this, 3, value); +}; + + +/** + * optional string query = 4; + * @return {string} + */ +proto.querier.v1.AnalyzeQueryRequest.prototype.getQuery = function() { + return /** @type {string} */ (jspb.Message.getFieldWithDefault(this, 4, "")); +}; + + +/** + * @param {string} value + * @return {!proto.querier.v1.AnalyzeQueryRequest} returns this + */ +proto.querier.v1.AnalyzeQueryRequest.prototype.setQuery = function(value) { + return jspb.Message.setProto3StringField(this, 4, value); +}; + + + +/** + * List of repeated fields within this message type. + * @private {!Array} + * @const + */ +proto.querier.v1.AnalyzeQueryResponse.repeatedFields_ = [1]; + + + +if (jspb.Message.GENERATE_TO_OBJECT) { +/** + * Creates an object representation of this proto. + * Field names that are reserved in JavaScript and will be renamed to pb_name. + * Optional fields that are not set will be set to undefined. + * To access a reserved field use, foo.pb_, eg, foo.pb_default. + * For the list of reserved names please see: + * net/proto2/compiler/js/internal/generator.cc#kKeyword. + * @param {boolean=} opt_includeInstance Deprecated. whether to include the + * JSPB instance for transitional soy proto support: + * http://goto/soy-param-migration + * @return {!Object} + */ +proto.querier.v1.AnalyzeQueryResponse.prototype.toObject = function(opt_includeInstance) { + return proto.querier.v1.AnalyzeQueryResponse.toObject(opt_includeInstance, this); +}; + + +/** + * Static version of the {@see toObject} method. + * @param {boolean|undefined} includeInstance Deprecated. Whether to include + * the JSPB instance for transitional soy proto support: + * http://goto/soy-param-migration + * @param {!proto.querier.v1.AnalyzeQueryResponse} msg The msg instance to transform. + * @return {!Object} + * @suppress {unusedLocalVariables} f is only used for nested messages + */ +proto.querier.v1.AnalyzeQueryResponse.toObject = function(includeInstance, msg) { + var f, obj = { + queryScopesList: jspb.Message.toObjectList(msg.getQueryScopesList(), + proto.querier.v1.QueryScope.toObject, includeInstance), + queryImpact: (f = msg.getQueryImpact()) && proto.querier.v1.QueryImpact.toObject(includeInstance, f) + }; + + if (includeInstance) { + obj.$jspbMessageInstance = msg; + } + return obj; +}; +} + + +/** + * Deserializes binary data (in protobuf wire format). + * @param {jspb.ByteSource} bytes The bytes to deserialize. + * @return {!proto.querier.v1.AnalyzeQueryResponse} + */ +proto.querier.v1.AnalyzeQueryResponse.deserializeBinary = function(bytes) { + var reader = new jspb.BinaryReader(bytes); + var msg = new proto.querier.v1.AnalyzeQueryResponse; + return proto.querier.v1.AnalyzeQueryResponse.deserializeBinaryFromReader(msg, reader); +}; + + +/** + * Deserializes binary data (in protobuf wire format) from the + * given reader into the given message object. + * @param {!proto.querier.v1.AnalyzeQueryResponse} msg The message object to deserialize into. + * @param {!jspb.BinaryReader} reader The BinaryReader to use. + * @return {!proto.querier.v1.AnalyzeQueryResponse} + */ +proto.querier.v1.AnalyzeQueryResponse.deserializeBinaryFromReader = function(msg, reader) { + while (reader.nextField()) { + if (reader.isEndGroup()) { + break; + } + var field = reader.getFieldNumber(); + switch (field) { + case 1: + var value = new proto.querier.v1.QueryScope; + reader.readMessage(value,proto.querier.v1.QueryScope.deserializeBinaryFromReader); + msg.addQueryScopes(value); + break; + case 2: + var value = new proto.querier.v1.QueryImpact; + reader.readMessage(value,proto.querier.v1.QueryImpact.deserializeBinaryFromReader); + msg.setQueryImpact(value); + break; + default: + reader.skipField(); + break; + } + } + return msg; +}; + + +/** + * Serializes the message to binary data (in protobuf wire format). + * @return {!Uint8Array} + */ +proto.querier.v1.AnalyzeQueryResponse.prototype.serializeBinary = function() { + var writer = new jspb.BinaryWriter(); + proto.querier.v1.AnalyzeQueryResponse.serializeBinaryToWriter(this, writer); + return writer.getResultBuffer(); +}; + + +/** + * Serializes the given message to binary data (in protobuf wire + * format), writing to the given BinaryWriter. + * @param {!proto.querier.v1.AnalyzeQueryResponse} message + * @param {!jspb.BinaryWriter} writer + * @suppress {unusedLocalVariables} f is only used for nested messages + */ +proto.querier.v1.AnalyzeQueryResponse.serializeBinaryToWriter = function(message, writer) { + var f = undefined; + f = message.getQueryScopesList(); + if (f.length > 0) { + writer.writeRepeatedMessage( + 1, + f, + proto.querier.v1.QueryScope.serializeBinaryToWriter + ); + } + f = message.getQueryImpact(); + if (f != null) { + writer.writeMessage( + 2, + f, + proto.querier.v1.QueryImpact.serializeBinaryToWriter + ); + } +}; + + +/** + * repeated QueryScope query_scopes = 1; + * @return {!Array} + */ +proto.querier.v1.AnalyzeQueryResponse.prototype.getQueryScopesList = function() { + return /** @type{!Array} */ ( + jspb.Message.getRepeatedWrapperField(this, proto.querier.v1.QueryScope, 1)); +}; + + +/** + * @param {!Array} value + * @return {!proto.querier.v1.AnalyzeQueryResponse} returns this +*/ +proto.querier.v1.AnalyzeQueryResponse.prototype.setQueryScopesList = function(value) { + return jspb.Message.setRepeatedWrapperField(this, 1, value); +}; + + +/** + * @param {!proto.querier.v1.QueryScope=} opt_value + * @param {number=} opt_index + * @return {!proto.querier.v1.QueryScope} + */ +proto.querier.v1.AnalyzeQueryResponse.prototype.addQueryScopes = function(opt_value, opt_index) { + return jspb.Message.addToRepeatedWrapperField(this, 1, opt_value, proto.querier.v1.QueryScope, opt_index); +}; + + +/** + * Clears the list making it empty but non-null. + * @return {!proto.querier.v1.AnalyzeQueryResponse} returns this + */ +proto.querier.v1.AnalyzeQueryResponse.prototype.clearQueryScopesList = function() { + return this.setQueryScopesList([]); +}; + + +/** + * optional QueryImpact query_impact = 2; + * @return {?proto.querier.v1.QueryImpact} + */ +proto.querier.v1.AnalyzeQueryResponse.prototype.getQueryImpact = function() { + return /** @type{?proto.querier.v1.QueryImpact} */ ( + jspb.Message.getWrapperField(this, proto.querier.v1.QueryImpact, 2)); +}; + + +/** + * @param {?proto.querier.v1.QueryImpact|undefined} value + * @return {!proto.querier.v1.AnalyzeQueryResponse} returns this +*/ +proto.querier.v1.AnalyzeQueryResponse.prototype.setQueryImpact = function(value) { + return jspb.Message.setWrapperField(this, 2, value); +}; + + +/** + * Clears the message field making it undefined. + * @return {!proto.querier.v1.AnalyzeQueryResponse} returns this + */ +proto.querier.v1.AnalyzeQueryResponse.prototype.clearQueryImpact = function() { + return this.setQueryImpact(undefined); +}; + + +/** + * Returns whether this field is set. + * @return {boolean} + */ +proto.querier.v1.AnalyzeQueryResponse.prototype.hasQueryImpact = function() { + return jspb.Message.getField(this, 2) != null; +}; + + + + + +if (jspb.Message.GENERATE_TO_OBJECT) { +/** + * Creates an object representation of this proto. + * Field names that are reserved in JavaScript and will be renamed to pb_name. + * Optional fields that are not set will be set to undefined. + * To access a reserved field use, foo.pb_, eg, foo.pb_default. + * For the list of reserved names please see: + * net/proto2/compiler/js/internal/generator.cc#kKeyword. + * @param {boolean=} opt_includeInstance Deprecated. whether to include the + * JSPB instance for transitional soy proto support: + * http://goto/soy-param-migration + * @return {!Object} + */ +proto.querier.v1.QueryScope.prototype.toObject = function(opt_includeInstance) { + return proto.querier.v1.QueryScope.toObject(opt_includeInstance, this); +}; + + +/** + * Static version of the {@see toObject} method. + * @param {boolean|undefined} includeInstance Deprecated. Whether to include + * the JSPB instance for transitional soy proto support: + * http://goto/soy-param-migration + * @param {!proto.querier.v1.QueryScope} msg The msg instance to transform. + * @return {!Object} + * @suppress {unusedLocalVariables} f is only used for nested messages + */ +proto.querier.v1.QueryScope.toObject = function(includeInstance, msg) { + var f, obj = { + componentType: jspb.Message.getFieldWithDefault(msg, 1, ""), + componentCount: jspb.Message.getFieldWithDefault(msg, 2, 0), + blockCount: jspb.Message.getFieldWithDefault(msg, 3, 0), + seriesCount: jspb.Message.getFieldWithDefault(msg, 4, 0), + profileCount: jspb.Message.getFieldWithDefault(msg, 5, 0), + sampleCount: jspb.Message.getFieldWithDefault(msg, 6, 0), + indexBytes: jspb.Message.getFieldWithDefault(msg, 7, 0), + profileBytes: jspb.Message.getFieldWithDefault(msg, 8, 0), + symbolBytes: jspb.Message.getFieldWithDefault(msg, 9, 0) + }; + + if (includeInstance) { + obj.$jspbMessageInstance = msg; + } + return obj; +}; +} + + +/** + * Deserializes binary data (in protobuf wire format). + * @param {jspb.ByteSource} bytes The bytes to deserialize. + * @return {!proto.querier.v1.QueryScope} + */ +proto.querier.v1.QueryScope.deserializeBinary = function(bytes) { + var reader = new jspb.BinaryReader(bytes); + var msg = new proto.querier.v1.QueryScope; + return proto.querier.v1.QueryScope.deserializeBinaryFromReader(msg, reader); +}; + + +/** + * Deserializes binary data (in protobuf wire format) from the + * given reader into the given message object. + * @param {!proto.querier.v1.QueryScope} msg The message object to deserialize into. + * @param {!jspb.BinaryReader} reader The BinaryReader to use. + * @return {!proto.querier.v1.QueryScope} + */ +proto.querier.v1.QueryScope.deserializeBinaryFromReader = function(msg, reader) { + while (reader.nextField()) { + if (reader.isEndGroup()) { + break; + } + var field = reader.getFieldNumber(); + switch (field) { + case 1: + var value = /** @type {string} */ (reader.readString()); + msg.setComponentType(value); + break; + case 2: + var value = /** @type {number} */ (reader.readUint64()); + msg.setComponentCount(value); + break; + case 3: + var value = /** @type {number} */ (reader.readUint64()); + msg.setBlockCount(value); + break; + case 4: + var value = /** @type {number} */ (reader.readUint64()); + msg.setSeriesCount(value); + break; + case 5: + var value = /** @type {number} */ (reader.readUint64()); + msg.setProfileCount(value); + break; + case 6: + var value = /** @type {number} */ (reader.readUint64()); + msg.setSampleCount(value); + break; + case 7: + var value = /** @type {number} */ (reader.readUint64()); + msg.setIndexBytes(value); + break; + case 8: + var value = /** @type {number} */ (reader.readUint64()); + msg.setProfileBytes(value); + break; + case 9: + var value = /** @type {number} */ (reader.readUint64()); + msg.setSymbolBytes(value); + break; + default: + reader.skipField(); + break; + } + } + return msg; +}; + + +/** + * Serializes the message to binary data (in protobuf wire format). + * @return {!Uint8Array} + */ +proto.querier.v1.QueryScope.prototype.serializeBinary = function() { + var writer = new jspb.BinaryWriter(); + proto.querier.v1.QueryScope.serializeBinaryToWriter(this, writer); + return writer.getResultBuffer(); +}; + + +/** + * Serializes the given message to binary data (in protobuf wire + * format), writing to the given BinaryWriter. + * @param {!proto.querier.v1.QueryScope} message + * @param {!jspb.BinaryWriter} writer + * @suppress {unusedLocalVariables} f is only used for nested messages + */ +proto.querier.v1.QueryScope.serializeBinaryToWriter = function(message, writer) { + var f = undefined; + f = message.getComponentType(); + if (f.length > 0) { + writer.writeString( + 1, + f + ); + } + f = message.getComponentCount(); + if (f !== 0) { + writer.writeUint64( + 2, + f + ); + } + f = message.getBlockCount(); + if (f !== 0) { + writer.writeUint64( + 3, + f + ); + } + f = message.getSeriesCount(); + if (f !== 0) { + writer.writeUint64( + 4, + f + ); + } + f = message.getProfileCount(); + if (f !== 0) { + writer.writeUint64( + 5, + f + ); + } + f = message.getSampleCount(); + if (f !== 0) { + writer.writeUint64( + 6, + f + ); + } + f = message.getIndexBytes(); + if (f !== 0) { + writer.writeUint64( + 7, + f + ); + } + f = message.getProfileBytes(); + if (f !== 0) { + writer.writeUint64( + 8, + f + ); + } + f = message.getSymbolBytes(); + if (f !== 0) { + writer.writeUint64( + 9, + f + ); + } +}; + + +/** + * optional string component_type = 1; + * @return {string} + */ +proto.querier.v1.QueryScope.prototype.getComponentType = function() { + return /** @type {string} */ (jspb.Message.getFieldWithDefault(this, 1, "")); +}; + + +/** + * @param {string} value + * @return {!proto.querier.v1.QueryScope} returns this + */ +proto.querier.v1.QueryScope.prototype.setComponentType = function(value) { + return jspb.Message.setProto3StringField(this, 1, value); +}; + + +/** + * optional uint64 component_count = 2; + * @return {number} + */ +proto.querier.v1.QueryScope.prototype.getComponentCount = function() { + return /** @type {number} */ (jspb.Message.getFieldWithDefault(this, 2, 0)); +}; + + +/** + * @param {number} value + * @return {!proto.querier.v1.QueryScope} returns this + */ +proto.querier.v1.QueryScope.prototype.setComponentCount = function(value) { + return jspb.Message.setProto3IntField(this, 2, value); +}; + + +/** + * optional uint64 block_count = 3; + * @return {number} + */ +proto.querier.v1.QueryScope.prototype.getBlockCount = function() { + return /** @type {number} */ (jspb.Message.getFieldWithDefault(this, 3, 0)); +}; + + +/** + * @param {number} value + * @return {!proto.querier.v1.QueryScope} returns this + */ +proto.querier.v1.QueryScope.prototype.setBlockCount = function(value) { + return jspb.Message.setProto3IntField(this, 3, value); +}; + + +/** + * optional uint64 series_count = 4; + * @return {number} + */ +proto.querier.v1.QueryScope.prototype.getSeriesCount = function() { + return /** @type {number} */ (jspb.Message.getFieldWithDefault(this, 4, 0)); +}; + + +/** + * @param {number} value + * @return {!proto.querier.v1.QueryScope} returns this + */ +proto.querier.v1.QueryScope.prototype.setSeriesCount = function(value) { + return jspb.Message.setProto3IntField(this, 4, value); +}; + + +/** + * optional uint64 profile_count = 5; + * @return {number} + */ +proto.querier.v1.QueryScope.prototype.getProfileCount = function() { + return /** @type {number} */ (jspb.Message.getFieldWithDefault(this, 5, 0)); +}; + + +/** + * @param {number} value + * @return {!proto.querier.v1.QueryScope} returns this + */ +proto.querier.v1.QueryScope.prototype.setProfileCount = function(value) { + return jspb.Message.setProto3IntField(this, 5, value); +}; + + +/** + * optional uint64 sample_count = 6; + * @return {number} + */ +proto.querier.v1.QueryScope.prototype.getSampleCount = function() { + return /** @type {number} */ (jspb.Message.getFieldWithDefault(this, 6, 0)); +}; + + +/** + * @param {number} value + * @return {!proto.querier.v1.QueryScope} returns this + */ +proto.querier.v1.QueryScope.prototype.setSampleCount = function(value) { + return jspb.Message.setProto3IntField(this, 6, value); +}; + + +/** + * optional uint64 index_bytes = 7; + * @return {number} + */ +proto.querier.v1.QueryScope.prototype.getIndexBytes = function() { + return /** @type {number} */ (jspb.Message.getFieldWithDefault(this, 7, 0)); +}; + + +/** + * @param {number} value + * @return {!proto.querier.v1.QueryScope} returns this + */ +proto.querier.v1.QueryScope.prototype.setIndexBytes = function(value) { + return jspb.Message.setProto3IntField(this, 7, value); +}; + + +/** + * optional uint64 profile_bytes = 8; + * @return {number} + */ +proto.querier.v1.QueryScope.prototype.getProfileBytes = function() { + return /** @type {number} */ (jspb.Message.getFieldWithDefault(this, 8, 0)); +}; + + +/** + * @param {number} value + * @return {!proto.querier.v1.QueryScope} returns this + */ +proto.querier.v1.QueryScope.prototype.setProfileBytes = function(value) { + return jspb.Message.setProto3IntField(this, 8, value); +}; + + +/** + * optional uint64 symbol_bytes = 9; + * @return {number} + */ +proto.querier.v1.QueryScope.prototype.getSymbolBytes = function() { + return /** @type {number} */ (jspb.Message.getFieldWithDefault(this, 9, 0)); +}; + + +/** + * @param {number} value + * @return {!proto.querier.v1.QueryScope} returns this + */ +proto.querier.v1.QueryScope.prototype.setSymbolBytes = function(value) { + return jspb.Message.setProto3IntField(this, 9, value); +}; + + + + + +if (jspb.Message.GENERATE_TO_OBJECT) { +/** + * Creates an object representation of this proto. + * Field names that are reserved in JavaScript and will be renamed to pb_name. + * Optional fields that are not set will be set to undefined. + * To access a reserved field use, foo.pb_, eg, foo.pb_default. + * For the list of reserved names please see: + * net/proto2/compiler/js/internal/generator.cc#kKeyword. + * @param {boolean=} opt_includeInstance Deprecated. whether to include the + * JSPB instance for transitional soy proto support: + * http://goto/soy-param-migration + * @return {!Object} + */ +proto.querier.v1.QueryImpact.prototype.toObject = function(opt_includeInstance) { + return proto.querier.v1.QueryImpact.toObject(opt_includeInstance, this); +}; + + +/** + * Static version of the {@see toObject} method. + * @param {boolean|undefined} includeInstance Deprecated. Whether to include + * the JSPB instance for transitional soy proto support: + * http://goto/soy-param-migration + * @param {!proto.querier.v1.QueryImpact} msg The msg instance to transform. + * @return {!Object} + * @suppress {unusedLocalVariables} f is only used for nested messages + */ +proto.querier.v1.QueryImpact.toObject = function(includeInstance, msg) { + var f, obj = { + totalBytesInTimeRange: jspb.Message.getFieldWithDefault(msg, 2, 0), + totalQueriedSeries: jspb.Message.getFieldWithDefault(msg, 3, 0), + deduplicationNeeded: jspb.Message.getBooleanFieldWithDefault(msg, 4, false) + }; + + if (includeInstance) { + obj.$jspbMessageInstance = msg; + } + return obj; +}; +} + + +/** + * Deserializes binary data (in protobuf wire format). + * @param {jspb.ByteSource} bytes The bytes to deserialize. + * @return {!proto.querier.v1.QueryImpact} + */ +proto.querier.v1.QueryImpact.deserializeBinary = function(bytes) { + var reader = new jspb.BinaryReader(bytes); + var msg = new proto.querier.v1.QueryImpact; + return proto.querier.v1.QueryImpact.deserializeBinaryFromReader(msg, reader); +}; + + +/** + * Deserializes binary data (in protobuf wire format) from the + * given reader into the given message object. + * @param {!proto.querier.v1.QueryImpact} msg The message object to deserialize into. + * @param {!jspb.BinaryReader} reader The BinaryReader to use. + * @return {!proto.querier.v1.QueryImpact} + */ +proto.querier.v1.QueryImpact.deserializeBinaryFromReader = function(msg, reader) { + while (reader.nextField()) { + if (reader.isEndGroup()) { + break; + } + var field = reader.getFieldNumber(); + switch (field) { + case 2: + var value = /** @type {number} */ (reader.readUint64()); + msg.setTotalBytesInTimeRange(value); + break; + case 3: + var value = /** @type {number} */ (reader.readUint64()); + msg.setTotalQueriedSeries(value); + break; + case 4: + var value = /** @type {boolean} */ (reader.readBool()); + msg.setDeduplicationNeeded(value); + break; + default: + reader.skipField(); + break; + } + } + return msg; +}; + + +/** + * Serializes the message to binary data (in protobuf wire format). + * @return {!Uint8Array} + */ +proto.querier.v1.QueryImpact.prototype.serializeBinary = function() { + var writer = new jspb.BinaryWriter(); + proto.querier.v1.QueryImpact.serializeBinaryToWriter(this, writer); + return writer.getResultBuffer(); +}; + + +/** + * Serializes the given message to binary data (in protobuf wire + * format), writing to the given BinaryWriter. + * @param {!proto.querier.v1.QueryImpact} message + * @param {!jspb.BinaryWriter} writer + * @suppress {unusedLocalVariables} f is only used for nested messages + */ +proto.querier.v1.QueryImpact.serializeBinaryToWriter = function(message, writer) { + var f = undefined; + f = message.getTotalBytesInTimeRange(); + if (f !== 0) { + writer.writeUint64( + 2, + f + ); + } + f = message.getTotalQueriedSeries(); + if (f !== 0) { + writer.writeUint64( + 3, + f + ); + } + f = message.getDeduplicationNeeded(); + if (f) { + writer.writeBool( + 4, + f + ); + } +}; + + +/** + * optional uint64 total_bytes_in_time_range = 2; + * @return {number} + */ +proto.querier.v1.QueryImpact.prototype.getTotalBytesInTimeRange = function() { + return /** @type {number} */ (jspb.Message.getFieldWithDefault(this, 2, 0)); +}; + + +/** + * @param {number} value + * @return {!proto.querier.v1.QueryImpact} returns this + */ +proto.querier.v1.QueryImpact.prototype.setTotalBytesInTimeRange = function(value) { + return jspb.Message.setProto3IntField(this, 2, value); +}; + + +/** + * optional uint64 total_queried_series = 3; + * @return {number} + */ +proto.querier.v1.QueryImpact.prototype.getTotalQueriedSeries = function() { + return /** @type {number} */ (jspb.Message.getFieldWithDefault(this, 3, 0)); +}; + + +/** + * @param {number} value + * @return {!proto.querier.v1.QueryImpact} returns this + */ +proto.querier.v1.QueryImpact.prototype.setTotalQueriedSeries = function(value) { + return jspb.Message.setProto3IntField(this, 3, value); +}; + + +/** + * optional bool deduplication_needed = 4; + * @return {boolean} + */ +proto.querier.v1.QueryImpact.prototype.getDeduplicationNeeded = function() { + return /** @type {boolean} */ (jspb.Message.getBooleanFieldWithDefault(this, 4, false)); +}; + + +/** + * @param {boolean} value + * @return {!proto.querier.v1.QueryImpact} returns this + */ +proto.querier.v1.QueryImpact.prototype.setDeduplicationNeeded = function(value) { + return jspb.Message.setProto3BooleanField(this, 4, value); +}; + + +/** + * @enum {number} + */ +proto.querier.v1.ProfileFormat = { + PROFILE_FORMAT_UNSPECIFIED: 0, + PROFILE_FORMAT_FLAMEGRAPH: 1, + PROFILE_FORMAT_TREE: 2 +}; + goog.object.extend(exports, proto.querier.v1); diff --git a/pyroscope/shared.js b/pyroscope/shared.js new file mode 100644 index 00000000..c954fb3f --- /dev/null +++ b/pyroscope/shared.js @@ -0,0 +1,27 @@ +/** + * + * @param payload {ReadableStream} + * @returns {Promise} + */ +const bufferize = async (payload) => { + const _body = [] + payload.on('data', data => { + _body.push(data)// += data.toString() + }) + if (payload.isPaused && payload.isPaused()) { + payload.resume() + } + await new Promise(resolve => { + payload.on('end', resolve) + payload.on('close', resolve) + }) + const body = Buffer.concat(_body) + if (body.length === 0) { + return null + } + return body +} + +module.exports = { + bufferize +} diff --git a/pyroscope/types/v1/types_pb.js b/pyroscope/types/v1/types_pb.js index d100d698..b24be923 100644 --- a/pyroscope/types/v1/types_pb.js +++ b/pyroscope/types/v1/types_pb.js @@ -23,6 +23,9 @@ var global = (function() { goog.exportSymbol('proto.types.v1.BlockCompaction', null, global); goog.exportSymbol('proto.types.v1.BlockInfo', null, global); +goog.exportSymbol('proto.types.v1.GetProfileStatsRequest', null, global); +goog.exportSymbol('proto.types.v1.GetProfileStatsResponse', null, global); +goog.exportSymbol('proto.types.v1.GoPGO', null, global); goog.exportSymbol('proto.types.v1.LabelNamesRequest', null, global); goog.exportSymbol('proto.types.v1.LabelNamesResponse', null, global); goog.exportSymbol('proto.types.v1.LabelPair', null, global); @@ -308,6 +311,69 @@ if (goog.DEBUG && !COMPILED) { */ proto.types.v1.Location.displayName = 'proto.types.v1.Location'; } +/** + * Generated by JsPbCodeGenerator. + * @param {Array=} opt_data Optional initial data array, typically from a + * server response, or constructed directly in Javascript. The array is used + * in place and becomes part of the constructed object. It is not cloned. + * If no data is provided, the constructed object will be empty, but still + * valid. + * @extends {jspb.Message} + * @constructor + */ +proto.types.v1.GoPGO = function(opt_data) { + jspb.Message.initialize(this, opt_data, 0, -1, null, null); +}; +goog.inherits(proto.types.v1.GoPGO, jspb.Message); +if (goog.DEBUG && !COMPILED) { + /** + * @public + * @override + */ + proto.types.v1.GoPGO.displayName = 'proto.types.v1.GoPGO'; +} +/** + * Generated by JsPbCodeGenerator. + * @param {Array=} opt_data Optional initial data array, typically from a + * server response, or constructed directly in Javascript. The array is used + * in place and becomes part of the constructed object. It is not cloned. + * If no data is provided, the constructed object will be empty, but still + * valid. + * @extends {jspb.Message} + * @constructor + */ +proto.types.v1.GetProfileStatsRequest = function(opt_data) { + jspb.Message.initialize(this, opt_data, 0, -1, null, null); +}; +goog.inherits(proto.types.v1.GetProfileStatsRequest, jspb.Message); +if (goog.DEBUG && !COMPILED) { + /** + * @public + * @override + */ + proto.types.v1.GetProfileStatsRequest.displayName = 'proto.types.v1.GetProfileStatsRequest'; +} +/** + * Generated by JsPbCodeGenerator. + * @param {Array=} opt_data Optional initial data array, typically from a + * server response, or constructed directly in Javascript. The array is used + * in place and becomes part of the constructed object. It is not cloned. + * If no data is provided, the constructed object will be empty, but still + * valid. + * @extends {jspb.Message} + * @constructor + */ +proto.types.v1.GetProfileStatsResponse = function(opt_data) { + jspb.Message.initialize(this, opt_data, 0, -1, null, null); +}; +goog.inherits(proto.types.v1.GetProfileStatsResponse, jspb.Message); +if (goog.DEBUG && !COMPILED) { + /** + * @public + * @override + */ + proto.types.v1.GetProfileStatsResponse.displayName = 'proto.types.v1.GetProfileStatsResponse'; +} @@ -2630,8 +2696,9 @@ proto.types.v1.StackTraceSelector.prototype.toObject = function(opt_includeInsta */ proto.types.v1.StackTraceSelector.toObject = function(includeInstance, msg) { var f, obj = { - stackTraceList: jspb.Message.toObjectList(msg.getStackTraceList(), - proto.types.v1.Location.toObject, includeInstance) + callSiteList: jspb.Message.toObjectList(msg.getCallSiteList(), + proto.types.v1.Location.toObject, includeInstance), + goPgo: (f = msg.getGoPgo()) && proto.types.v1.GoPGO.toObject(includeInstance, f) }; if (includeInstance) { @@ -2671,7 +2738,12 @@ proto.types.v1.StackTraceSelector.deserializeBinaryFromReader = function(msg, re case 1: var value = new proto.types.v1.Location; reader.readMessage(value,proto.types.v1.Location.deserializeBinaryFromReader); - msg.addStackTrace(value); + msg.addCallSite(value); + break; + case 2: + var value = new proto.types.v1.GoPGO; + reader.readMessage(value,proto.types.v1.GoPGO.deserializeBinaryFromReader); + msg.setGoPgo(value); break; default: reader.skipField(); @@ -2702,7 +2774,7 @@ proto.types.v1.StackTraceSelector.prototype.serializeBinary = function() { */ proto.types.v1.StackTraceSelector.serializeBinaryToWriter = function(message, writer) { var f = undefined; - f = message.getStackTraceList(); + f = message.getCallSiteList(); if (f.length > 0) { writer.writeRepeatedMessage( 1, @@ -2710,14 +2782,22 @@ proto.types.v1.StackTraceSelector.serializeBinaryToWriter = function(message, wr proto.types.v1.Location.serializeBinaryToWriter ); } + f = message.getGoPgo(); + if (f != null) { + writer.writeMessage( + 2, + f, + proto.types.v1.GoPGO.serializeBinaryToWriter + ); + } }; /** - * repeated Location stack_trace = 1; + * repeated Location call_site = 1; * @return {!Array} */ -proto.types.v1.StackTraceSelector.prototype.getStackTraceList = function() { +proto.types.v1.StackTraceSelector.prototype.getCallSiteList = function() { return /** @type{!Array} */ ( jspb.Message.getRepeatedWrapperField(this, proto.types.v1.Location, 1)); }; @@ -2727,7 +2807,7 @@ proto.types.v1.StackTraceSelector.prototype.getStackTraceList = function() { * @param {!Array} value * @return {!proto.types.v1.StackTraceSelector} returns this */ -proto.types.v1.StackTraceSelector.prototype.setStackTraceList = function(value) { +proto.types.v1.StackTraceSelector.prototype.setCallSiteList = function(value) { return jspb.Message.setRepeatedWrapperField(this, 1, value); }; @@ -2737,7 +2817,7 @@ proto.types.v1.StackTraceSelector.prototype.setStackTraceList = function(value) * @param {number=} opt_index * @return {!proto.types.v1.Location} */ -proto.types.v1.StackTraceSelector.prototype.addStackTrace = function(opt_value, opt_index) { +proto.types.v1.StackTraceSelector.prototype.addCallSite = function(opt_value, opt_index) { return jspb.Message.addToRepeatedWrapperField(this, 1, opt_value, proto.types.v1.Location, opt_index); }; @@ -2746,8 +2826,45 @@ proto.types.v1.StackTraceSelector.prototype.addStackTrace = function(opt_value, * Clears the list making it empty but non-null. * @return {!proto.types.v1.StackTraceSelector} returns this */ -proto.types.v1.StackTraceSelector.prototype.clearStackTraceList = function() { - return this.setStackTraceList([]); +proto.types.v1.StackTraceSelector.prototype.clearCallSiteList = function() { + return this.setCallSiteList([]); +}; + + +/** + * optional GoPGO go_pgo = 2; + * @return {?proto.types.v1.GoPGO} + */ +proto.types.v1.StackTraceSelector.prototype.getGoPgo = function() { + return /** @type{?proto.types.v1.GoPGO} */ ( + jspb.Message.getWrapperField(this, proto.types.v1.GoPGO, 2)); +}; + + +/** + * @param {?proto.types.v1.GoPGO|undefined} value + * @return {!proto.types.v1.StackTraceSelector} returns this +*/ +proto.types.v1.StackTraceSelector.prototype.setGoPgo = function(value) { + return jspb.Message.setWrapperField(this, 2, value); +}; + + +/** + * Clears the message field making it undefined. + * @return {!proto.types.v1.StackTraceSelector} returns this + */ +proto.types.v1.StackTraceSelector.prototype.clearGoPgo = function() { + return this.setGoPgo(undefined); +}; + + +/** + * Returns whether this field is set. + * @return {boolean} + */ +proto.types.v1.StackTraceSelector.prototype.hasGoPgo = function() { + return jspb.Message.getField(this, 2) != null; }; @@ -2881,6 +2998,457 @@ proto.types.v1.Location.prototype.setName = function(value) { }; + + + +if (jspb.Message.GENERATE_TO_OBJECT) { +/** + * Creates an object representation of this proto. + * Field names that are reserved in JavaScript and will be renamed to pb_name. + * Optional fields that are not set will be set to undefined. + * To access a reserved field use, foo.pb_, eg, foo.pb_default. + * For the list of reserved names please see: + * net/proto2/compiler/js/internal/generator.cc#kKeyword. + * @param {boolean=} opt_includeInstance Deprecated. whether to include the + * JSPB instance for transitional soy proto support: + * http://goto/soy-param-migration + * @return {!Object} + */ +proto.types.v1.GoPGO.prototype.toObject = function(opt_includeInstance) { + return proto.types.v1.GoPGO.toObject(opt_includeInstance, this); +}; + + +/** + * Static version of the {@see toObject} method. + * @param {boolean|undefined} includeInstance Deprecated. Whether to include + * the JSPB instance for transitional soy proto support: + * http://goto/soy-param-migration + * @param {!proto.types.v1.GoPGO} msg The msg instance to transform. + * @return {!Object} + * @suppress {unusedLocalVariables} f is only used for nested messages + */ +proto.types.v1.GoPGO.toObject = function(includeInstance, msg) { + var f, obj = { + keepLocations: jspb.Message.getFieldWithDefault(msg, 1, 0), + aggregateCallees: jspb.Message.getBooleanFieldWithDefault(msg, 2, false) + }; + + if (includeInstance) { + obj.$jspbMessageInstance = msg; + } + return obj; +}; +} + + +/** + * Deserializes binary data (in protobuf wire format). + * @param {jspb.ByteSource} bytes The bytes to deserialize. + * @return {!proto.types.v1.GoPGO} + */ +proto.types.v1.GoPGO.deserializeBinary = function(bytes) { + var reader = new jspb.BinaryReader(bytes); + var msg = new proto.types.v1.GoPGO; + return proto.types.v1.GoPGO.deserializeBinaryFromReader(msg, reader); +}; + + +/** + * Deserializes binary data (in protobuf wire format) from the + * given reader into the given message object. + * @param {!proto.types.v1.GoPGO} msg The message object to deserialize into. + * @param {!jspb.BinaryReader} reader The BinaryReader to use. + * @return {!proto.types.v1.GoPGO} + */ +proto.types.v1.GoPGO.deserializeBinaryFromReader = function(msg, reader) { + while (reader.nextField()) { + if (reader.isEndGroup()) { + break; + } + var field = reader.getFieldNumber(); + switch (field) { + case 1: + var value = /** @type {number} */ (reader.readUint32()); + msg.setKeepLocations(value); + break; + case 2: + var value = /** @type {boolean} */ (reader.readBool()); + msg.setAggregateCallees(value); + break; + default: + reader.skipField(); + break; + } + } + return msg; +}; + + +/** + * Serializes the message to binary data (in protobuf wire format). + * @return {!Uint8Array} + */ +proto.types.v1.GoPGO.prototype.serializeBinary = function() { + var writer = new jspb.BinaryWriter(); + proto.types.v1.GoPGO.serializeBinaryToWriter(this, writer); + return writer.getResultBuffer(); +}; + + +/** + * Serializes the given message to binary data (in protobuf wire + * format), writing to the given BinaryWriter. + * @param {!proto.types.v1.GoPGO} message + * @param {!jspb.BinaryWriter} writer + * @suppress {unusedLocalVariables} f is only used for nested messages + */ +proto.types.v1.GoPGO.serializeBinaryToWriter = function(message, writer) { + var f = undefined; + f = message.getKeepLocations(); + if (f !== 0) { + writer.writeUint32( + 1, + f + ); + } + f = message.getAggregateCallees(); + if (f) { + writer.writeBool( + 2, + f + ); + } +}; + + +/** + * optional uint32 keep_locations = 1; + * @return {number} + */ +proto.types.v1.GoPGO.prototype.getKeepLocations = function() { + return /** @type {number} */ (jspb.Message.getFieldWithDefault(this, 1, 0)); +}; + + +/** + * @param {number} value + * @return {!proto.types.v1.GoPGO} returns this + */ +proto.types.v1.GoPGO.prototype.setKeepLocations = function(value) { + return jspb.Message.setProto3IntField(this, 1, value); +}; + + +/** + * optional bool aggregate_callees = 2; + * @return {boolean} + */ +proto.types.v1.GoPGO.prototype.getAggregateCallees = function() { + return /** @type {boolean} */ (jspb.Message.getBooleanFieldWithDefault(this, 2, false)); +}; + + +/** + * @param {boolean} value + * @return {!proto.types.v1.GoPGO} returns this + */ +proto.types.v1.GoPGO.prototype.setAggregateCallees = function(value) { + return jspb.Message.setProto3BooleanField(this, 2, value); +}; + + + + + +if (jspb.Message.GENERATE_TO_OBJECT) { +/** + * Creates an object representation of this proto. + * Field names that are reserved in JavaScript and will be renamed to pb_name. + * Optional fields that are not set will be set to undefined. + * To access a reserved field use, foo.pb_, eg, foo.pb_default. + * For the list of reserved names please see: + * net/proto2/compiler/js/internal/generator.cc#kKeyword. + * @param {boolean=} opt_includeInstance Deprecated. whether to include the + * JSPB instance for transitional soy proto support: + * http://goto/soy-param-migration + * @return {!Object} + */ +proto.types.v1.GetProfileStatsRequest.prototype.toObject = function(opt_includeInstance) { + return proto.types.v1.GetProfileStatsRequest.toObject(opt_includeInstance, this); +}; + + +/** + * Static version of the {@see toObject} method. + * @param {boolean|undefined} includeInstance Deprecated. Whether to include + * the JSPB instance for transitional soy proto support: + * http://goto/soy-param-migration + * @param {!proto.types.v1.GetProfileStatsRequest} msg The msg instance to transform. + * @return {!Object} + * @suppress {unusedLocalVariables} f is only used for nested messages + */ +proto.types.v1.GetProfileStatsRequest.toObject = function(includeInstance, msg) { + var f, obj = { + + }; + + if (includeInstance) { + obj.$jspbMessageInstance = msg; + } + return obj; +}; +} + + +/** + * Deserializes binary data (in protobuf wire format). + * @param {jspb.ByteSource} bytes The bytes to deserialize. + * @return {!proto.types.v1.GetProfileStatsRequest} + */ +proto.types.v1.GetProfileStatsRequest.deserializeBinary = function(bytes) { + var reader = new jspb.BinaryReader(bytes); + var msg = new proto.types.v1.GetProfileStatsRequest; + return proto.types.v1.GetProfileStatsRequest.deserializeBinaryFromReader(msg, reader); +}; + + +/** + * Deserializes binary data (in protobuf wire format) from the + * given reader into the given message object. + * @param {!proto.types.v1.GetProfileStatsRequest} msg The message object to deserialize into. + * @param {!jspb.BinaryReader} reader The BinaryReader to use. + * @return {!proto.types.v1.GetProfileStatsRequest} + */ +proto.types.v1.GetProfileStatsRequest.deserializeBinaryFromReader = function(msg, reader) { + while (reader.nextField()) { + if (reader.isEndGroup()) { + break; + } + var field = reader.getFieldNumber(); + switch (field) { + default: + reader.skipField(); + break; + } + } + return msg; +}; + + +/** + * Serializes the message to binary data (in protobuf wire format). + * @return {!Uint8Array} + */ +proto.types.v1.GetProfileStatsRequest.prototype.serializeBinary = function() { + var writer = new jspb.BinaryWriter(); + proto.types.v1.GetProfileStatsRequest.serializeBinaryToWriter(this, writer); + return writer.getResultBuffer(); +}; + + +/** + * Serializes the given message to binary data (in protobuf wire + * format), writing to the given BinaryWriter. + * @param {!proto.types.v1.GetProfileStatsRequest} message + * @param {!jspb.BinaryWriter} writer + * @suppress {unusedLocalVariables} f is only used for nested messages + */ +proto.types.v1.GetProfileStatsRequest.serializeBinaryToWriter = function(message, writer) { + var f = undefined; +}; + + + + + +if (jspb.Message.GENERATE_TO_OBJECT) { +/** + * Creates an object representation of this proto. + * Field names that are reserved in JavaScript and will be renamed to pb_name. + * Optional fields that are not set will be set to undefined. + * To access a reserved field use, foo.pb_, eg, foo.pb_default. + * For the list of reserved names please see: + * net/proto2/compiler/js/internal/generator.cc#kKeyword. + * @param {boolean=} opt_includeInstance Deprecated. whether to include the + * JSPB instance for transitional soy proto support: + * http://goto/soy-param-migration + * @return {!Object} + */ +proto.types.v1.GetProfileStatsResponse.prototype.toObject = function(opt_includeInstance) { + return proto.types.v1.GetProfileStatsResponse.toObject(opt_includeInstance, this); +}; + + +/** + * Static version of the {@see toObject} method. + * @param {boolean|undefined} includeInstance Deprecated. Whether to include + * the JSPB instance for transitional soy proto support: + * http://goto/soy-param-migration + * @param {!proto.types.v1.GetProfileStatsResponse} msg The msg instance to transform. + * @return {!Object} + * @suppress {unusedLocalVariables} f is only used for nested messages + */ +proto.types.v1.GetProfileStatsResponse.toObject = function(includeInstance, msg) { + var f, obj = { + dataIngested: jspb.Message.getBooleanFieldWithDefault(msg, 1, false), + oldestProfileTime: jspb.Message.getFieldWithDefault(msg, 2, 0), + newestProfileTime: jspb.Message.getFieldWithDefault(msg, 3, 0) + }; + + if (includeInstance) { + obj.$jspbMessageInstance = msg; + } + return obj; +}; +} + + +/** + * Deserializes binary data (in protobuf wire format). + * @param {jspb.ByteSource} bytes The bytes to deserialize. + * @return {!proto.types.v1.GetProfileStatsResponse} + */ +proto.types.v1.GetProfileStatsResponse.deserializeBinary = function(bytes) { + var reader = new jspb.BinaryReader(bytes); + var msg = new proto.types.v1.GetProfileStatsResponse; + return proto.types.v1.GetProfileStatsResponse.deserializeBinaryFromReader(msg, reader); +}; + + +/** + * Deserializes binary data (in protobuf wire format) from the + * given reader into the given message object. + * @param {!proto.types.v1.GetProfileStatsResponse} msg The message object to deserialize into. + * @param {!jspb.BinaryReader} reader The BinaryReader to use. + * @return {!proto.types.v1.GetProfileStatsResponse} + */ +proto.types.v1.GetProfileStatsResponse.deserializeBinaryFromReader = function(msg, reader) { + while (reader.nextField()) { + if (reader.isEndGroup()) { + break; + } + var field = reader.getFieldNumber(); + switch (field) { + case 1: + var value = /** @type {boolean} */ (reader.readBool()); + msg.setDataIngested(value); + break; + case 2: + var value = /** @type {number} */ (reader.readInt64()); + msg.setOldestProfileTime(value); + break; + case 3: + var value = /** @type {number} */ (reader.readInt64()); + msg.setNewestProfileTime(value); + break; + default: + reader.skipField(); + break; + } + } + return msg; +}; + + +/** + * Serializes the message to binary data (in protobuf wire format). + * @return {!Uint8Array} + */ +proto.types.v1.GetProfileStatsResponse.prototype.serializeBinary = function() { + var writer = new jspb.BinaryWriter(); + proto.types.v1.GetProfileStatsResponse.serializeBinaryToWriter(this, writer); + return writer.getResultBuffer(); +}; + + +/** + * Serializes the given message to binary data (in protobuf wire + * format), writing to the given BinaryWriter. + * @param {!proto.types.v1.GetProfileStatsResponse} message + * @param {!jspb.BinaryWriter} writer + * @suppress {unusedLocalVariables} f is only used for nested messages + */ +proto.types.v1.GetProfileStatsResponse.serializeBinaryToWriter = function(message, writer) { + var f = undefined; + f = message.getDataIngested(); + if (f) { + writer.writeBool( + 1, + f + ); + } + f = message.getOldestProfileTime(); + if (f !== 0) { + writer.writeInt64( + 2, + f + ); + } + f = message.getNewestProfileTime(); + if (f !== 0) { + writer.writeInt64( + 3, + f + ); + } +}; + + +/** + * optional bool data_ingested = 1; + * @return {boolean} + */ +proto.types.v1.GetProfileStatsResponse.prototype.getDataIngested = function() { + return /** @type {boolean} */ (jspb.Message.getBooleanFieldWithDefault(this, 1, false)); +}; + + +/** + * @param {boolean} value + * @return {!proto.types.v1.GetProfileStatsResponse} returns this + */ +proto.types.v1.GetProfileStatsResponse.prototype.setDataIngested = function(value) { + return jspb.Message.setProto3BooleanField(this, 1, value); +}; + + +/** + * optional int64 oldest_profile_time = 2; + * @return {number} + */ +proto.types.v1.GetProfileStatsResponse.prototype.getOldestProfileTime = function() { + return /** @type {number} */ (jspb.Message.getFieldWithDefault(this, 2, 0)); +}; + + +/** + * @param {number} value + * @return {!proto.types.v1.GetProfileStatsResponse} returns this + */ +proto.types.v1.GetProfileStatsResponse.prototype.setOldestProfileTime = function(value) { + return jspb.Message.setProto3IntField(this, 2, value); +}; + + +/** + * optional int64 newest_profile_time = 3; + * @return {number} + */ +proto.types.v1.GetProfileStatsResponse.prototype.getNewestProfileTime = function() { + return /** @type {number} */ (jspb.Message.getFieldWithDefault(this, 3, 0)); +}; + + +/** + * @param {number} value + * @return {!proto.types.v1.GetProfileStatsResponse} returns this + */ +proto.types.v1.GetProfileStatsResponse.prototype.setNewestProfileTime = function(value) { + return jspb.Message.setProto3IntField(this, 3, value); +}; + + /** * @enum {number} */ From 505b6b50514c6225c9e2aec5a431f020b9bcf757 Mon Sep 17 00:00:00 2001 From: akvlad Date: Thu, 1 Aug 2024 19:49:50 +0300 Subject: [PATCH 2/8] refactor; render endpoint --- pyroscope/flamebearer.d.ts | 53 ++ pyroscope/json_parsers.js | 20 +- pyroscope/merge_stack_traces.js | 163 ++++++ pyroscope/proto/settings.proto | 28 + pyroscope/pyroscope.js | 347 ++----------- pyroscope/render.js | 137 +++++ pyroscope/settings.js | 31 ++ pyroscope/settings_grpc_pb.js | 77 +++ pyroscope/settings_pb.js | 887 ++++++++++++++++++++++++++++++++ pyroscope/shared.js | 158 +++++- 10 files changed, 1595 insertions(+), 306 deletions(-) create mode 100644 pyroscope/flamebearer.d.ts create mode 100644 pyroscope/merge_stack_traces.js create mode 100644 pyroscope/proto/settings.proto create mode 100644 pyroscope/render.js create mode 100644 pyroscope/settings.js create mode 100644 pyroscope/settings_grpc_pb.js create mode 100644 pyroscope/settings_pb.js diff --git a/pyroscope/flamebearer.d.ts b/pyroscope/flamebearer.d.ts new file mode 100644 index 00000000..a62150ea --- /dev/null +++ b/pyroscope/flamebearer.d.ts @@ -0,0 +1,53 @@ + +type int64 = string; +type uint64 = string; +type units = string; + +export interface Flamebearer { + version: number, + flamebearerProfileV1: flamebearerProfileV1 + telemetry: {[key: string]: any} +} + +export interface flamebearerProfileV1 { + flamebearer: flamebearerV1, + metadata: flamebearerMetadataV1, + timeline: flamebearerTimelineV1, + heatmap: heatmap, + leftTicks: string, + rightTicks: string, +} + +export interface flamebearerV1 { + names: string, + levels: [[number]], + numTicks: number, + maxSelf: number +} + +export interface flamebearerMetadataV1 { + format: string, + spyName: string, + sampleRate: number, + units: units, + name: string +} + +export interface flamebearerTimelineV1 { + startTime: int64, + samples: [uint64] + durationDelta: int64, + watermarks: {[key: number]: int64} +} + +export interface heatmap { + values: [[uint64]], + timeBuckets: int64, + valueBuckets: int64, + startTime: int64, + endTime: int64, + minValue: uint64, + maxValue: uint64, + minDepth: uint64, + maxDepth: uint64 +} diff --git a/pyroscope/json_parsers.js b/pyroscope/json_parsers.js index 37cd6eca..48d8e27a 100644 --- a/pyroscope/json_parsers.js +++ b/pyroscope/json_parsers.js @@ -21,6 +21,11 @@ const getProfileStats = async (req, payload) => { return null } +const settingsGet = async (req, payload) => { + req.type = 'json' + return {} +} + const labelNames = async (req, payload) => { req.type = 'json' let body = await bufferize(payload) @@ -32,8 +37,21 @@ const labelNames = async (req, payload) => { } } +const analyzeQuery = async (req, payload) => { + req.type = 'json' + let body = await bufferize(payload) + body = JSON.parse(body.toString()) + return { + getStart: () => body.start, + getEnd: () => body.end, + getQuery: () => body.query + } +} + module.exports = { series, getProfileStats, - labelNames + labelNames, + settingsGet, + analyzeQuery } diff --git a/pyroscope/merge_stack_traces.js b/pyroscope/merge_stack_traces.js new file mode 100644 index 00000000..c9033f4e --- /dev/null +++ b/pyroscope/merge_stack_traces.js @@ -0,0 +1,163 @@ +const { checkVersion, DATABASE_NAME } = require('../lib/utils') +const Sql = require('@cloki/clickhouse-sql') +const { clusterName } = require('../common') +const clickhouse = require('../lib/db/clickhouse') +const { readULeb32 } = require('./pprof') +const pprofBin = require('./pprof-bin/pkg') +const { + serviceNameSelectorQuery, + labelSelectorQuery +} = require('./shared') + +const sqlWithReference = (ref) => { + const res = new Sql.WithReference(ref) + res.toString = function () { + if (this.ref.inline) { + return `(${this.ref.query.toString()}) as ${this.ref.alias}` + } + return this.ref.alias + } + return res +} + +let ctxIdx = 0 + +const mergeStackTraces = async (typeRegex, sel, fromTimeSec, toTimeSec, log) => { + const dist = clusterName ? '_dist' : '' + const v2 = checkVersion('profiles_v2', (fromTimeSec - 3600) * 1000) + const serviceNameSelector = serviceNameSelectorQuery(sel) + const typeIdSelector = Sql.Eq( + 'type_id', + Sql.val(`${typeRegex.type}:${typeRegex.periodType}:${typeRegex.periodUnit}`) + ) + const idxSelect = (new Sql.Select()) + .select('fingerprint') + .from(`${DATABASE_NAME()}.profiles_series_gin`) + .where( + Sql.And( + Sql.Eq(new Sql.Raw(`has(sample_types_units, (${Sql.quoteVal(typeRegex.sampleType)},${Sql.quoteVal(typeRegex.sampleUnit)}))`), 1), + typeIdSelector, + Sql.Gte('date', new Sql.Raw(`toDate(FROM_UNIXTIME(${Math.floor(fromTimeSec)}))`)), + Sql.Lte('date', new Sql.Raw(`toDate(FROM_UNIXTIME(${Math.floor(toTimeSec)}))`)), + serviceNameSelector + ) + ).groupBy('fingerprint') + labelSelectorQuery(idxSelect, sel) + const withIdxSelect = new Sql.With('idx', idxSelect, !!clusterName) + const rawReq = (new Sql.Select()).with(withIdxSelect) + .select([ + new Sql.Raw(`arrayMap(x -> (x.1, x.2, x.3, (arrayFirst(y -> y.1 == ${Sql.quoteVal(`${typeRegex.sampleType}:${typeRegex.sampleUnit}`)}, x.4) as af).2, af.3), tree)`), + 'tree' + ], 'functions') + .from(`${DATABASE_NAME()}.profiles${dist}`) + .where( + Sql.And( + Sql.Gte('timestamp_ns', new Sql.Raw(Math.floor(fromTimeSec) + '000000000')), + Sql.Lte('timestamp_ns', new Sql.Raw(Math.floor(toTimeSec) + '000000000')), + new Sql.In('fingerprint', 'IN', sqlWithReference(withIdxSelect)), + typeIdSelector, + serviceNameSelector + )) + if (process.env.ADVANCED_PROFILES_MERGE_LIMIT) { + rawReq.orderBy(['timestamp_ns', 'desc']).limit(parseInt(process.env.ADVANCED_PROFILES_MERGE_LIMIT)) + } + const withRawReq = new Sql.With('raw', rawReq, !!clusterName) + const joinedReq = (new Sql.Select()).with(withRawReq).select([ + new Sql.Raw('(raw.tree.1, raw.tree.2, raw.tree.3, sum(raw.tree.4), sum(raw.tree.5))'), + 'tree2' + ]).from(sqlWithReference(withRawReq)) + .join('raw.tree', 'array') + .groupBy(new Sql.Raw('raw.tree.1'), new Sql.Raw('raw.tree.2'), new Sql.Raw('raw.tree.3')) + .orderBy(new Sql.Raw('raw.tree.1')).limit(2000000) + const withJoinedReq = new Sql.With('joined', joinedReq, !!clusterName) + const joinedAggregatedReq = (new Sql.Select()).select( + [new Sql.Raw('groupArray(tree2)'), 'tree']).from(sqlWithReference(withJoinedReq)) + const functionsReq = (new Sql.Select()).select( + [new Sql.Raw('groupUniqArray(raw.functions)'), 'functions2'] + ).from(sqlWithReference(withRawReq)).join('raw.functions', 'array') + + let brackLegacy = (new Sql.Select()).select( + [new Sql.Raw('[]::Array(String)'), 'legacy'] + ) + let withLegacy = null + if (!v2) { + const legacy = (new Sql.Select()).with(withIdxSelect) + .select('payload') + .from(`${DATABASE_NAME()}.profiles${dist}`) + .where( + Sql.And( + Sql.Gte('timestamp_ns', new Sql.Raw(Math.floor(fromTimeSec) + '000000000')), + Sql.Lte('timestamp_ns', new Sql.Raw(Math.floor(toTimeSec) + '000000000')), + new Sql.In('fingerprint', 'IN', sqlWithReference(withIdxSelect)), + Sql.Eq(new Sql.Raw('empty(tree)'), 1), + typeIdSelector, + serviceNameSelector + )) + if (process.env.ADVANCED_PROFILES_MERGE_LIMIT) { + legacy.orderBy(['timestamp_ns', 'desc']).limit(parseInt(process.env.ADVANCED_PROFILES_MERGE_LIMIT)) + } + withLegacy = new Sql.With('legacy', legacy, !!clusterName) + brackLegacy = (new Sql.Select()) + .select([new Sql.Raw('groupArray(payload)'), 'payloads']) + .from(sqlWithReference(withLegacy)) + } + brackLegacy = new Sql.Raw(`(${brackLegacy.toString()})`) + const brack1 = new Sql.Raw(`(${joinedAggregatedReq.toString()})`) + const brack2 = new Sql.Raw(`(${functionsReq.toString()})`) + + const sqlReq = (new Sql.Select()) + .select( + [brackLegacy, 'legacy'], + [brack2, 'functions'], + [brack1, 'tree'] + ) + if (v2) { + sqlReq.with(withJoinedReq, withRawReq) + } else { + sqlReq.with(withJoinedReq, withRawReq, withLegacy) + } + + let start = Date.now() + const profiles = await clickhouse.rawRequest(sqlReq.toString() + ' FORMAT RowBinary', + null, + DATABASE_NAME(), + { + responseType: 'arraybuffer' + }) + const binData = Uint8Array.from(profiles.data) + log.debug(`selectMergeStacktraces: profiles downloaded: ${binData.length / 1025}kB in ${Date.now() - start}ms`) + require('./pprof-bin/pkg/pprof_bin').init_panic_hook() + const _ctxIdx = ++ctxIdx + const [legacyLen, shift] = readULeb32(binData, 0) + let ofs = shift + try { + let mergePprofLat = BigInt(0) + for (let i = 0; i < legacyLen; i++) { + const [profLen, shift] = readULeb32(binData, ofs) + ofs += shift + start = process.hrtime?.bigint ? process.hrtime.bigint() : BigInt(0) + pprofBin.merge_prof(_ctxIdx, + Uint8Array.from(profiles.data.slice(ofs, ofs + profLen)), + `${typeRegex.sampleType}:${typeRegex.sampleUnit}`) + mergePprofLat += (process.hrtime?.bigint ? process.hrtime.bigint() : BigInt(0)) - start + ofs += profLen + } + start = process.hrtime?.bigint ? process.hrtime.bigint() : BigInt(0) + pprofBin.merge_tree(_ctxIdx, Uint8Array.from(profiles.data.slice(ofs)), + typeRegex.sampleType + ':' + typeRegex.sampleUnit) + const mergeTreeLat = (process.hrtime?.bigint ? process.hrtime.bigint() : BigInt(0)) - start + start = process.hrtime?.bigint ? process.hrtime.bigint() : BigInt(0) + const resp = pprofBin.export_tree(_ctxIdx, typeRegex.sampleType + ':' + typeRegex.sampleUnit) + const exportTreeLat = (process.hrtime?.bigint ? process.hrtime.bigint() : BigInt(0)) - start + log.debug(`merge_pprof: ${mergePprofLat / BigInt(1000000)}ms`) + log.debug(`merge_tree: ${mergeTreeLat / BigInt(1000000)}ms`) + log.debug(`export_tree: ${exportTreeLat / BigInt(1000000)}ms`) + return Buffer.from(resp) + } finally { + try { pprofBin.drop_tree(_ctxIdx) } catch (e) {} + } +} + +module.exports = { + mergeStackTraces +} \ No newline at end of file diff --git a/pyroscope/proto/settings.proto b/pyroscope/proto/settings.proto new file mode 100644 index 00000000..fb7375bf --- /dev/null +++ b/pyroscope/proto/settings.proto @@ -0,0 +1,28 @@ +syntax = "proto3"; + +package settings.v1; + +service SettingsService { + rpc Get(GetSettingsRequest) returns (GetSettingsResponse) {} + rpc Set(SetSettingsRequest) returns (SetSettingsResponse) {} +} + +message GetSettingsRequest {} + +message GetSettingsResponse { + repeated Setting settings = 1; +} + +message SetSettingsRequest { + Setting setting = 1; +} + +message SetSettingsResponse { + Setting setting = 1; +} + +message Setting { + string name = 1; + string value = 2; + int64 modifiedAt = 3; +} diff --git a/pyroscope/pyroscope.js b/pyroscope/pyroscope.js index b9cdbcf2..f8976192 100644 --- a/pyroscope/pyroscope.js +++ b/pyroscope/pyroscope.js @@ -2,49 +2,18 @@ const messages = require('./querier_pb') const types = require('./types/v1/types_pb') const services = require('./querier_grpc_pb') const clickhouse = require('../lib/db/clickhouse') -const { DATABASE_NAME, checkVersion } = require('../lib/utils') +const { DATABASE_NAME } = require('../lib/utils') const Sql = require('@cloki/clickhouse-sql') -const compiler = require('../parser/bnf') -const { readULeb32 } = require('./pprof') const pprofBin = require('./pprof-bin/pkg/pprof_bin') const { QrynBadRequest } = require('../lib/handlers/errors') const { clusterName } = require('../common') const logger = require('../lib/logger') const jsonParsers = require('./json_parsers') -const { bufferize } = require('./shared') +const { parser, wrapResponse, parseTypeId, serviceNameSelectorQuery, labelSelectorQuery } = require('./shared') const HISTORY_TIMESPAN = 1000 * 60 * 60 * 24 * 7 - -/** - * - * @param typeId {string} - */ -const parseTypeId = (typeId) => { - const typeParts = typeId.match(/^([^:]+):([^:]+):([^:]+):([^:]+):([^:]+)$/) - if (!typeParts) { - throw new QrynBadRequest('invalid type id') - } - return { - type: typeParts[1], - sampleType: typeParts[2], - sampleUnit: typeParts[3], - periodType: typeParts[4], - periodUnit: typeParts[5] - } -} - -const wrapResponse = (hndl) => { - return async (req, res) => { - const _res = await hndl(req, res) - if (!_res || !_res.serializeBinary) { - return _res - } - if (req.type === 'json') { - const strRes = JSON.stringify(normalizeProtoResponse(_res.toObject())) - return res.code(200).send(strRes) - } - return res.code(200).send(Buffer.from(_res.serializeBinary())) - } -} +const settings = require('./settings') +const { mergeStackTraces } = require('./merge_stack_traces') +const render = require('./render') const profileTypesHandler = async (req, res) => { const dist = clusterName ? '_dist' : '' @@ -72,7 +41,7 @@ WHERE date >= toDate(FROM_UNIXTIME(${Math.floor(fromTimeSec)})) AND date <= toDa pt.setPeriodUnit(periodUnit) return pt })) - return _res //res.code(200).send(Buffer.from(_res.serializeBinary())) + return _res } const labelNames = async (req, res) => { @@ -89,7 +58,7 @@ WHERE date >= toDate(FROM_UNIXTIME(${Math.floor(fromTimeSec)})) AND date <= toDa null, DATABASE_NAME()) const resp = new types.LabelNamesResponse() resp.setNamesList(labelNames.data.data.map(label => label.key)) - return resp //res.code(200).send(Buffer.from(resp.serializeBinary())) + return resp } const labelValues = async (req, res) => { @@ -113,118 +82,14 @@ date >= toDate(FROM_UNIXTIME(${Math.floor(fromTimeSec)})) AND date <= toDate(FROM_UNIXTIME(${Math.floor(toTimeSec)})) FORMAT JSON`, null, DATABASE_NAME()) const resp = new types.LabelValuesResponse() resp.setNamesList(labelValues.data.data.map(label => label.val)) - return resp //res.code(200).send(Buffer.from(resp.serializeBinary())) -} - -const parser = (MsgClass) => { - return async (req, payload) => { - const body = await bufferize(payload) - req._rawBody = body - return MsgClass.deserializeBinary(body) - } -} - -let ctxIdx = 0 - -/** - * - * @param {Sql.Select} query - * @param {string} labelSelector - */ -const labelSelectorQuery = (query, labelSelector) => { - if (!labelSelector || !labelSelector.length || labelSelector === '{}') { - return query - } - const labelSelectorScript = parseLabelSelector(labelSelector) - const labelsConds = [] - for (const rule of labelSelectorScript.Children('log_stream_selector_rule')) { - const val = JSON.parse(rule.Child('quoted_str').value) - let valRul = null - switch (rule.Child('operator').value) { - case '=': - valRul = Sql.Eq(new Sql.Raw('val'), Sql.val(val)) - break - case '!=': - valRul = Sql.Ne(new Sql.Raw('val'), Sql.val(val)) - break - case '=~': - valRul = Sql.Eq(new Sql.Raw(`match(val, ${Sql.quoteVal(val)})`), 1) - break - case '!~': - valRul = Sql.Ne(new Sql.Raw(`match(val, ${Sql.quoteVal(val)})`), 1) - } - const labelSubCond = Sql.And( - Sql.Eq('key', Sql.val(rule.Child('label').value)), - valRul - ) - labelsConds.push(labelSubCond) - } - query.where(Sql.Or(...labelsConds)) - query.groupBy(new Sql.Raw('fingerprint')) - query.having(Sql.Eq( - new Sql.Raw(`groupBitOr(${labelsConds.map((cond, i) => { - return `bitShiftLeft(toUInt64(${cond}), ${i})` - }).join('+')})`), - new Sql.Raw(`bitShiftLeft(toUInt64(1), ${labelsConds.length})-1`) - )) -} - -const parseLabelSelector = (labelSelector) => { - if (labelSelector.endsWith(',}')) { - labelSelector = labelSelector.slice(0, -2) + '}' - } - return compiler.ParseScript(labelSelector).rootToken -} - -const serviceNameSelectorQuery = (labelSelector) => { - const empty = Sql.Eq(new Sql.Raw('1'), new Sql.Raw('1')) - if (!labelSelector || !labelSelector.length || labelSelector === '{}') { - return empty - } - const labelSelectorScript = parseLabelSelector(labelSelector) - let conds = null - for (const rule of labelSelectorScript.Children('log_stream_selector_rule')) { - const label = rule.Child('label').value - if (label !== 'service_name') { - continue - } - const val = JSON.parse(rule.Child('quoted_str').value) - let valRul = null - switch (rule.Child('operator').value) { - case '=': - valRul = Sql.Eq(new Sql.Raw('service_name'), Sql.val(val)) - break - case '!=': - valRul = Sql.Ne(new Sql.Raw('service_name'), Sql.val(val)) - break - case '=~': - valRul = Sql.Eq(new Sql.Raw(`match(service_name, ${Sql.quoteVal(val)})`), 1) - break - case '!~': - valRul = Sql.Ne(new Sql.Raw(`match(service_name, ${Sql.quoteVal(val)})`), 1) - } - conds = valRul - } - return conds || empty + return resp } const selectMergeStacktraces = async (req, res) => { return await selectMergeStacktracesV2(req, res) } -const sqlWithReference = (ref) => { - const res = new Sql.WithReference(ref) - res.toString = function () { - if (this.ref.inline) { - return `(${this.ref.query.toString()}) as ${this.ref.alias}` - } - return this.ref.alias - } - return res -} - const selectMergeStacktracesV2 = async (req, res) => { - const dist = clusterName ? '_dist' : '' const typeRegex = parseTypeId(req.body.getProfileTypeid()) const sel = req.body.getLabelSelector() const fromTimeSec = req.body && req.body.getStart() @@ -233,138 +98,8 @@ const selectMergeStacktracesV2 = async (req, res) => { const toTimeSec = req.body && req.body.getEnd() ? Math.floor(parseInt(req.body.getEnd()) / 1000) : Math.floor(Date.now() / 1000) - const v2 = checkVersion('profiles_v2', (fromTimeSec - 3600) * 1000) - const serviceNameSelector = serviceNameSelectorQuery(sel) - const typeIdSelector = Sql.Eq( - 'type_id', - Sql.val(`${typeRegex.type}:${typeRegex.periodType}:${typeRegex.periodUnit}`) - ) - const idxSelect = (new Sql.Select()) - .select('fingerprint') - .from(`${DATABASE_NAME()}.profiles_series_gin`) - .where( - Sql.And( - Sql.Eq(new Sql.Raw(`has(sample_types_units, (${Sql.quoteVal(typeRegex.sampleType)},${Sql.quoteVal(typeRegex.sampleUnit)}))`), 1), - typeIdSelector, - Sql.Gte('date', new Sql.Raw(`toDate(FROM_UNIXTIME(${Math.floor(fromTimeSec)}))`)), - Sql.Lte('date', new Sql.Raw(`toDate(FROM_UNIXTIME(${Math.floor(toTimeSec)}))`)), - serviceNameSelector - ) - ).groupBy('fingerprint') - labelSelectorQuery(idxSelect, sel) - const withIdxSelect = new Sql.With('idx', idxSelect, !!clusterName) - const rawReq = (new Sql.Select()).with(withIdxSelect) - .select([ - new Sql.Raw(`arrayMap(x -> (x.1, x.2, x.3, (arrayFirst(y -> y.1 == ${Sql.quoteVal(`${typeRegex.sampleType}:${typeRegex.sampleUnit}`)}, x.4) as af).2, af.3), tree)`), - 'tree' - ], 'functions') - .from(`${DATABASE_NAME()}.profiles${dist}`) - .where( - Sql.And( - Sql.Gte('timestamp_ns', new Sql.Raw(Math.floor(fromTimeSec) + '000000000')), - Sql.Lte('timestamp_ns', new Sql.Raw(Math.floor(toTimeSec) + '000000000')), - new Sql.In('fingerprint', 'IN', sqlWithReference(withIdxSelect)), - typeIdSelector, - serviceNameSelector - )) - if (process.env.ADVANCED_PROFILES_MERGE_LIMIT) { - rawReq.orderBy(['timestamp_ns', 'desc']).limit(parseInt(process.env.ADVANCED_PROFILES_MERGE_LIMIT)) - } - const withRawReq = new Sql.With('raw', rawReq, !!clusterName) - const joinedReq = (new Sql.Select()).with(withRawReq).select([ - new Sql.Raw('(raw.tree.1, raw.tree.2, raw.tree.3, sum(raw.tree.4), sum(raw.tree.5))'), - 'tree2' - ]).from(sqlWithReference(withRawReq)) - .join('raw.tree', 'array') - .groupBy(new Sql.Raw('raw.tree.1'), new Sql.Raw('raw.tree.2'), new Sql.Raw('raw.tree.3')) - .orderBy(new Sql.Raw('raw.tree.1')).limit(2000000) - const withJoinedReq = new Sql.With('joined', joinedReq, !!clusterName) - const joinedAggregatedReq = (new Sql.Select()).select( - [new Sql.Raw('groupArray(tree2)'), 'tree']).from(sqlWithReference(withJoinedReq)) - const functionsReq = (new Sql.Select()).select( - [new Sql.Raw('groupUniqArray(raw.functions)'), 'functions2'] - ).from(sqlWithReference(withRawReq)).join('raw.functions', 'array') - - let brackLegacy = (new Sql.Select()).select( - [new Sql.Raw('[]::Array(String)'), 'legacy'] - ) - let withLegacy = null - if (!v2) { - const legacy = (new Sql.Select()).with(withIdxSelect) - .select('payload') - .from(`${DATABASE_NAME()}.profiles${dist}`) - .where( - Sql.And( - Sql.Gte('timestamp_ns', new Sql.Raw(Math.floor(fromTimeSec) + '000000000')), - Sql.Lte('timestamp_ns', new Sql.Raw(Math.floor(toTimeSec) + '000000000')), - new Sql.In('fingerprint', 'IN', sqlWithReference(withIdxSelect)), - Sql.Eq(new Sql.Raw('empty(tree)'), 1), - typeIdSelector, - serviceNameSelector - )) - if (process.env.ADVANCED_PROFILES_MERGE_LIMIT) { - legacy.orderBy(['timestamp_ns', 'desc']).limit(parseInt(process.env.ADVANCED_PROFILES_MERGE_LIMIT)) - } - withLegacy = new Sql.With('legacy', legacy, !!clusterName) - brackLegacy = (new Sql.Select()) - .select([new Sql.Raw('groupArray(payload)'), 'payloads']) - .from(sqlWithReference(withLegacy)) - } - brackLegacy = new Sql.Raw(`(${brackLegacy.toString()})`) - const brack1 = new Sql.Raw(`(${joinedAggregatedReq.toString()})`) - const brack2 = new Sql.Raw(`(${functionsReq.toString()})`) - - const sqlReq = (new Sql.Select()) - .select( - [brackLegacy, 'legacy'], - [brack2, 'functions'], - [brack1, 'tree'] - ) - if (v2) { - sqlReq.with(withJoinedReq, withRawReq) - } else { - sqlReq.with(withJoinedReq, withRawReq, withLegacy) - } - - let start = Date.now() - const profiles = await clickhouse.rawRequest(sqlReq.toString() + ' FORMAT RowBinary', - null, - DATABASE_NAME(), - { - responseType: 'arraybuffer' - }) - const binData = Uint8Array.from(profiles.data) - req.log.debug(`selectMergeStacktraces: profiles downloaded: ${binData.length / 1025}kB in ${Date.now() - start}ms`) - require('./pprof-bin/pkg/pprof_bin').init_panic_hook() - const _ctxIdx = ++ctxIdx - const [legacyLen, shift] = readULeb32(binData, 0) - let ofs = shift - try { - let mergePprofLat = BigInt(0) - for (let i = 0; i < legacyLen; i++) { - const [profLen, shift] = readULeb32(binData, ofs) - ofs += shift - start = process.hrtime?.bigint ? process.hrtime.bigint() : BigInt(0) - pprofBin.merge_prof(_ctxIdx, - Uint8Array.from(profiles.data.slice(ofs, ofs + profLen)), - `${typeRegex.sampleType}:${typeRegex.sampleUnit}`) - mergePprofLat += (process.hrtime?.bigint ? process.hrtime.bigint() : BigInt(0)) - start - ofs += profLen - } - start = process.hrtime?.bigint ? process.hrtime.bigint() : BigInt(0) - pprofBin.merge_tree(_ctxIdx, Uint8Array.from(profiles.data.slice(ofs)), - typeRegex.sampleType + ':' + typeRegex.sampleUnit) - const mergeTreeLat = (process.hrtime?.bigint ? process.hrtime.bigint() : BigInt(0)) - start - start = process.hrtime?.bigint ? process.hrtime.bigint() : BigInt(0) - const resp = pprofBin.export_tree(_ctxIdx, typeRegex.sampleType + ':' + typeRegex.sampleUnit) - const exportTreeLat = (process.hrtime?.bigint ? process.hrtime.bigint() : BigInt(0)) - start - req.log.debug(`merge_pprof: ${mergePprofLat / BigInt(1000000)}ms`) - req.log.debug(`merge_tree: ${mergeTreeLat / BigInt(1000000)}ms`) - req.log.debug(`export_tree: ${exportTreeLat / BigInt(1000000)}ms`) - return res.code(200).send(Buffer.from(resp)) - } finally { - try { pprofBin.drop_tree(_ctxIdx) } catch (e) {} - } + const resBuffer = await mergeStackTraces(typeRegex, sel, fromTimeSec, toTimeSec, req.log) + return res.code(200).send(resBuffer) } const selectSeries = async (req, res) => { @@ -499,7 +234,7 @@ const selectSeries = async (req, res) => { const resp = new messages.SelectSeriesResponse() resp.setSeriesList(seriesList) - return resp //res.code(200).send(Buffer.from(resp.serializeBinary())) + return resp } const selectMergeProfile = async (req, res) => { @@ -626,7 +361,7 @@ const series = async (req, res) => { .where( Sql.And( Sql.Gte('date', new Sql.Raw(`toDate(FROM_UNIXTIME(${Math.floor(fromTimeSec)}))`)), - Sql.Lte('date', new Sql.Raw(`toDate(FROM_UNIXTIME(${Math.floor(toTimeSec)}))`)), + Sql.Lte('date', new Sql.Raw(`toDate(FROM_UNIXTIME(${Math.floor(toTimeSec)}))`)) ) ) promises.push(clickhouse.rawRequest(labelsReq.toString() + ' FORMAT JSON', null, DATABASE_NAME())) @@ -671,30 +406,7 @@ const series = async (req, res) => { } }) response.setLabelsSetList(labelsSet) - return response //res.code(200).send(Buffer.from(response.serializeBinary())) -} - -/** - * - * @param proto {Object} - */ -const normalizeProtoResponse = (proto) => { - if (typeof proto !== 'object') { - return proto - } - return Object.fromEntries(Object.entries(proto).map((e) => { - let name = e[0] - if (name.endsWith('List')) { - name = name.slice(0, -4) - } - if (Array.isArray(e[1])) { - return [name, e[1].map(normalizeProtoResponse)] - } - if (typeof e[1] === 'object') { - return [name, normalizeProtoResponse(e[1])] - } - return [name, e[1]] - })) + return response } /** @@ -796,7 +508,30 @@ select response.setDataIngested(!!sqlRes.data.data[0].non_empty) response.setOldestProfileTime(sqlRes.data.data[0].min_time) response.setNewestProfileTime(sqlRes.data.data[0].max_time) - return response//res.code(200).send(Buffer.from(response.serializeBinary())) + return response +} + +const analyzeQuery = async (req, res) => { + const query = req.body.getQuery() + const fromTimeSec = Math.floor(req.getStart && req.getStart() + ? parseInt(req.getStart()) / 1000 + : Date.now() / 1000 - HISTORY_TIMESPAN) + const toTimeSec = Math.floor(req.getEnd && req.getEnd() + ? parseInt(req.getEnd()) / 1000 + : Date.now() / 1000) + console.log(query) + + const scope = new messages.QueryScope() + scope.setComponentType('store') + scope.setComponentCount(1) + const impact = new messages.QueryImpact() + impact.setTotalBytesInTimeRange(10 * 1024 * 1024) + impact.setTotalQueriedSeries(15) + impact.setDeduplicationNeeded(false) + const response = new messages.AnalyzeQueryResponse() + response.setQueryScopesList([scope]) + response.setQueryImpact(impact) + return response } module.exports.init = (fastify) => { @@ -808,12 +543,14 @@ module.exports.init = (fastify) => { selectSeries: selectSeries, selectMergeProfile: selectMergeProfile, series: series, - getProfileStats: getProfileStats + getProfileStats: getProfileStats, + analyzeQuery: analyzeQuery } const parsers = { series: jsonParsers.series, getProfileStats: jsonParsers.getProfileStats, - labelNames: jsonParsers.labelNames + labelNames: jsonParsers.labelNames, + analyzeQuery: jsonParsers.analyzeQuery } for (const name of Object.keys(fns)) { fastify.post(services.QuerierServiceService[name].path, (req, res) => { @@ -823,4 +560,6 @@ module.exports.init = (fastify) => { '*': parser(services.QuerierServiceService[name].requestType) }) } + settings.init(fastify) + render.init(fastify) } diff --git a/pyroscope/render.js b/pyroscope/render.js new file mode 100644 index 00000000..1d93e3e8 --- /dev/null +++ b/pyroscope/render.js @@ -0,0 +1,137 @@ +const { parseTypeId } = require('./shared') +const { mergeStackTraces } = require('./merge_stack_traces') + +const render = async (req, res) => { + const query = req.query.query + const parsedQuery = parseQuery(query) + const fromTimeSec = req.query.from + ? Math.floor(parseInt(req.query.from) / 1000) + : Math.floor((Date.now() - 1000 * 60 * 60 * 48) / 1000) + const toTimeSec = req.query.until + ? Math.floor(parseInt(req.query.from) / 1000) + : Math.floor((Date.now() - 1000 * 60 * 60 * 48) / 1000) + if (!parsedQuery) { + return res.sendStatus(400).send('Invalid query') + } + const groupBy = req.query.groupBy + let agg = '' + switch (req.query.aggregation) { + case 'sum': + agg = 'sum' + break + case 'avg': + agg = 'avg' + break + } + if (req.query.format === 'dot') { + return res.sendStatus(400).send('Dot format is not supported') + } + const mergeStackTrace = mergeStackTraces( + parsedQuery.typeDesc, + parsedQuery.labelSelector, + fromTimeSec, + toTimeSec, + req.log) + //TODO +} + +/* +func (q *QueryHandlers) Render(w http.ResponseWriter, req *http.Request) { + var resFlame *connect.Response[querierv1.SelectMergeStacktracesResponse] + g, ctx := errgroup.WithContext(req.Context()) + selectParamsClone := selectParams.CloneVT() + g.Go(func() error { + var err error + resFlame, err = q.client.SelectMergeStacktraces(ctx, connect.NewRequest(selectParamsClone)) + return err + }) + + timelineStep := timeline.CalcPointInterval(selectParams.Start, selectParams.End) + var resSeries *connect.Response[querierv1.SelectSeriesResponse] + g.Go(func() error { + var err error + resSeries, err = q.client.SelectSeries(req.Context(), + connect.NewRequest(&querierv1.SelectSeriesRequest{ + ProfileTypeID: selectParams.ProfileTypeID, + LabelSelector: selectParams.LabelSelector, + Start: selectParams.Start, + End: selectParams.End, + Step: timelineStep, + GroupBy: groupBy, + Aggregation: &aggregation, + })) + + return err + }) + + err = g.Wait() + if err != nil { + httputil.Error(w, err) + return + } + + seriesVal := &typesv1.Series{} + if len(resSeries.Msg.Series) == 1 { + seriesVal = resSeries.Msg.Series[0] + } + + fb := phlaremodel.ExportToFlamebearer(resFlame.Msg.Flamegraph, profileType) + fb.Timeline = timeline.New(seriesVal, selectParams.Start, selectParams.End, int64(timelineStep)) + + if len(groupBy) > 0 { + fb.Groups = make(map[string]*flamebearer.FlamebearerTimelineV1) + for _, s := range resSeries.Msg.Series { + key := "*" + for _, l := range s.Labels { + // right now we only support one group by + if l.Name == groupBy[0] { + key = l.Value + break + } + } + fb.Groups[key] = timeline.New(s, selectParams.Start, selectParams.End, int64(timelineStep)) + } + } + + w.Header().Add("Content-Type", "application/json") + if err := json.NewEncoder(w).Encode(fb); err != nil { + httputil.Error(w, err) + return + } +} + */ + +/** + * + * @param query {string} + */ +const parseQuery = (query) => { + query = query.trim() + const match = query.match(/^([^{]+)\s*(\{(.*)})?$/) + if (!match) { + return null + } + const typeId = match[1] + const typeDesc = parseTypeId(typeId) + const strLabels = match[3] || '' + const labels = [] + if (strLabels && strLabels !== '') { + for (const m in strLabels.matchAll(/([,{])\s*([^A-Za-z0-9_]+)\s*(!=|!~|=~|=)\s*("([^"\\]|\\.)*")/g)) { + labels.push([m[2], m[3], m[4]]) + } + } + return { + typeId, + typeDesc, + labels, + labelSelector: strLabels + } +} + +const init = (fastify) => { + fastify.get('/pyroscope/render', render) +} + +module.exports = { + init +} diff --git a/pyroscope/settings.js b/pyroscope/settings.js new file mode 100644 index 00000000..98b5dc42 --- /dev/null +++ b/pyroscope/settings.js @@ -0,0 +1,31 @@ +const messages = require('./settings_pb') +const services = require('./settings_grpc_pb') +const { parser, wrapResponse } = require('./shared') +const parsers = require('./json_parsers') + +const get = (req, res) => { + const _res = new messages.GetSettingsResponse() + const s = new messages.Setting() + s.setName('pluginSettings') + s.setValue('{}') + s.setModifiedat(Date.now()) + _res.setSettingsList([s]) + return _res +} + +module.exports.init = (fastify) => { + const fns = { + get: get + } + const jsonParsers = { + get: parsers.settingsGet + } + for (const name of Object.keys(fns)) { + fastify.post(services.SettingsServiceService[name].path, (req, res) => { + return wrapResponse(fns[name])(req, res) + }, { + 'application/json': jsonParsers[name], + '*': parser(services.SettingsServiceService[name].requestType) + }) + } +} diff --git a/pyroscope/settings_grpc_pb.js b/pyroscope/settings_grpc_pb.js new file mode 100644 index 00000000..0797ea17 --- /dev/null +++ b/pyroscope/settings_grpc_pb.js @@ -0,0 +1,77 @@ +// GENERATED CODE -- DO NOT EDIT! + +'use strict'; +var grpc = require('@grpc/grpc-js'); +var settings_pb = require('./settings_pb.js'); + +function serialize_settings_v1_GetSettingsRequest(arg) { + if (!(arg instanceof settings_pb.GetSettingsRequest)) { + throw new Error('Expected argument of type settings.v1.GetSettingsRequest'); + } + return Buffer.from(arg.serializeBinary()); +} + +function deserialize_settings_v1_GetSettingsRequest(buffer_arg) { + return settings_pb.GetSettingsRequest.deserializeBinary(new Uint8Array(buffer_arg)); +} + +function serialize_settings_v1_GetSettingsResponse(arg) { + if (!(arg instanceof settings_pb.GetSettingsResponse)) { + throw new Error('Expected argument of type settings.v1.GetSettingsResponse'); + } + return Buffer.from(arg.serializeBinary()); +} + +function deserialize_settings_v1_GetSettingsResponse(buffer_arg) { + return settings_pb.GetSettingsResponse.deserializeBinary(new Uint8Array(buffer_arg)); +} + +function serialize_settings_v1_SetSettingsRequest(arg) { + if (!(arg instanceof settings_pb.SetSettingsRequest)) { + throw new Error('Expected argument of type settings.v1.SetSettingsRequest'); + } + return Buffer.from(arg.serializeBinary()); +} + +function deserialize_settings_v1_SetSettingsRequest(buffer_arg) { + return settings_pb.SetSettingsRequest.deserializeBinary(new Uint8Array(buffer_arg)); +} + +function serialize_settings_v1_SetSettingsResponse(arg) { + if (!(arg instanceof settings_pb.SetSettingsResponse)) { + throw new Error('Expected argument of type settings.v1.SetSettingsResponse'); + } + return Buffer.from(arg.serializeBinary()); +} + +function deserialize_settings_v1_SetSettingsResponse(buffer_arg) { + return settings_pb.SetSettingsResponse.deserializeBinary(new Uint8Array(buffer_arg)); +} + + +var SettingsServiceService = exports.SettingsServiceService = { + get: { + path: '/settings.v1.SettingsService/Get', + requestStream: false, + responseStream: false, + requestType: settings_pb.GetSettingsRequest, + responseType: settings_pb.GetSettingsResponse, + requestSerialize: serialize_settings_v1_GetSettingsRequest, + requestDeserialize: deserialize_settings_v1_GetSettingsRequest, + responseSerialize: serialize_settings_v1_GetSettingsResponse, + responseDeserialize: deserialize_settings_v1_GetSettingsResponse, + }, + set: { + path: '/settings.v1.SettingsService/Set', + requestStream: false, + responseStream: false, + requestType: settings_pb.SetSettingsRequest, + responseType: settings_pb.SetSettingsResponse, + requestSerialize: serialize_settings_v1_SetSettingsRequest, + requestDeserialize: deserialize_settings_v1_SetSettingsRequest, + responseSerialize: serialize_settings_v1_SetSettingsResponse, + responseDeserialize: deserialize_settings_v1_SetSettingsResponse, + }, +}; + +exports.SettingsServiceClient = grpc.makeGenericClientConstructor(SettingsServiceService); diff --git a/pyroscope/settings_pb.js b/pyroscope/settings_pb.js new file mode 100644 index 00000000..c8a073e7 --- /dev/null +++ b/pyroscope/settings_pb.js @@ -0,0 +1,887 @@ +// source: settings.proto +/** + * @fileoverview + * @enhanceable + * @suppress {missingRequire} reports error on implicit type usages. + * @suppress {messageConventions} JS Compiler reports an error if a variable or + * field starts with 'MSG_' and isn't a translatable message. + * @public + */ +// GENERATED CODE -- DO NOT EDIT! +/* eslint-disable */ +// @ts-nocheck + +var jspb = require('google-protobuf'); +var goog = jspb; +var global = (function() { + if (this) { return this; } + if (typeof window !== 'undefined') { return window; } + if (typeof global !== 'undefined') { return global; } + if (typeof self !== 'undefined') { return self; } + return Function('return this')(); +}.call(null)); + +goog.exportSymbol('proto.settings.v1.GetSettingsRequest', null, global); +goog.exportSymbol('proto.settings.v1.GetSettingsResponse', null, global); +goog.exportSymbol('proto.settings.v1.SetSettingsRequest', null, global); +goog.exportSymbol('proto.settings.v1.SetSettingsResponse', null, global); +goog.exportSymbol('proto.settings.v1.Setting', null, global); +/** + * Generated by JsPbCodeGenerator. + * @param {Array=} opt_data Optional initial data array, typically from a + * server response, or constructed directly in Javascript. The array is used + * in place and becomes part of the constructed object. It is not cloned. + * If no data is provided, the constructed object will be empty, but still + * valid. + * @extends {jspb.Message} + * @constructor + */ +proto.settings.v1.GetSettingsRequest = function(opt_data) { + jspb.Message.initialize(this, opt_data, 0, -1, null, null); +}; +goog.inherits(proto.settings.v1.GetSettingsRequest, jspb.Message); +if (goog.DEBUG && !COMPILED) { + /** + * @public + * @override + */ + proto.settings.v1.GetSettingsRequest.displayName = 'proto.settings.v1.GetSettingsRequest'; +} +/** + * Generated by JsPbCodeGenerator. + * @param {Array=} opt_data Optional initial data array, typically from a + * server response, or constructed directly in Javascript. The array is used + * in place and becomes part of the constructed object. It is not cloned. + * If no data is provided, the constructed object will be empty, but still + * valid. + * @extends {jspb.Message} + * @constructor + */ +proto.settings.v1.GetSettingsResponse = function(opt_data) { + jspb.Message.initialize(this, opt_data, 0, -1, proto.settings.v1.GetSettingsResponse.repeatedFields_, null); +}; +goog.inherits(proto.settings.v1.GetSettingsResponse, jspb.Message); +if (goog.DEBUG && !COMPILED) { + /** + * @public + * @override + */ + proto.settings.v1.GetSettingsResponse.displayName = 'proto.settings.v1.GetSettingsResponse'; +} +/** + * Generated by JsPbCodeGenerator. + * @param {Array=} opt_data Optional initial data array, typically from a + * server response, or constructed directly in Javascript. The array is used + * in place and becomes part of the constructed object. It is not cloned. + * If no data is provided, the constructed object will be empty, but still + * valid. + * @extends {jspb.Message} + * @constructor + */ +proto.settings.v1.SetSettingsRequest = function(opt_data) { + jspb.Message.initialize(this, opt_data, 0, -1, null, null); +}; +goog.inherits(proto.settings.v1.SetSettingsRequest, jspb.Message); +if (goog.DEBUG && !COMPILED) { + /** + * @public + * @override + */ + proto.settings.v1.SetSettingsRequest.displayName = 'proto.settings.v1.SetSettingsRequest'; +} +/** + * Generated by JsPbCodeGenerator. + * @param {Array=} opt_data Optional initial data array, typically from a + * server response, or constructed directly in Javascript. The array is used + * in place and becomes part of the constructed object. It is not cloned. + * If no data is provided, the constructed object will be empty, but still + * valid. + * @extends {jspb.Message} + * @constructor + */ +proto.settings.v1.SetSettingsResponse = function(opt_data) { + jspb.Message.initialize(this, opt_data, 0, -1, null, null); +}; +goog.inherits(proto.settings.v1.SetSettingsResponse, jspb.Message); +if (goog.DEBUG && !COMPILED) { + /** + * @public + * @override + */ + proto.settings.v1.SetSettingsResponse.displayName = 'proto.settings.v1.SetSettingsResponse'; +} +/** + * Generated by JsPbCodeGenerator. + * @param {Array=} opt_data Optional initial data array, typically from a + * server response, or constructed directly in Javascript. The array is used + * in place and becomes part of the constructed object. It is not cloned. + * If no data is provided, the constructed object will be empty, but still + * valid. + * @extends {jspb.Message} + * @constructor + */ +proto.settings.v1.Setting = function(opt_data) { + jspb.Message.initialize(this, opt_data, 0, -1, null, null); +}; +goog.inherits(proto.settings.v1.Setting, jspb.Message); +if (goog.DEBUG && !COMPILED) { + /** + * @public + * @override + */ + proto.settings.v1.Setting.displayName = 'proto.settings.v1.Setting'; +} + + + +if (jspb.Message.GENERATE_TO_OBJECT) { +/** + * Creates an object representation of this proto. + * Field names that are reserved in JavaScript and will be renamed to pb_name. + * Optional fields that are not set will be set to undefined. + * To access a reserved field use, foo.pb_, eg, foo.pb_default. + * For the list of reserved names please see: + * net/proto2/compiler/js/internal/generator.cc#kKeyword. + * @param {boolean=} opt_includeInstance Deprecated. whether to include the + * JSPB instance for transitional soy proto support: + * http://goto/soy-param-migration + * @return {!Object} + */ +proto.settings.v1.GetSettingsRequest.prototype.toObject = function(opt_includeInstance) { + return proto.settings.v1.GetSettingsRequest.toObject(opt_includeInstance, this); +}; + + +/** + * Static version of the {@see toObject} method. + * @param {boolean|undefined} includeInstance Deprecated. Whether to include + * the JSPB instance for transitional soy proto support: + * http://goto/soy-param-migration + * @param {!proto.settings.v1.GetSettingsRequest} msg The msg instance to transform. + * @return {!Object} + * @suppress {unusedLocalVariables} f is only used for nested messages + */ +proto.settings.v1.GetSettingsRequest.toObject = function(includeInstance, msg) { + var f, obj = { + + }; + + if (includeInstance) { + obj.$jspbMessageInstance = msg; + } + return obj; +}; +} + + +/** + * Deserializes binary data (in protobuf wire format). + * @param {jspb.ByteSource} bytes The bytes to deserialize. + * @return {!proto.settings.v1.GetSettingsRequest} + */ +proto.settings.v1.GetSettingsRequest.deserializeBinary = function(bytes) { + var reader = new jspb.BinaryReader(bytes); + var msg = new proto.settings.v1.GetSettingsRequest; + return proto.settings.v1.GetSettingsRequest.deserializeBinaryFromReader(msg, reader); +}; + + +/** + * Deserializes binary data (in protobuf wire format) from the + * given reader into the given message object. + * @param {!proto.settings.v1.GetSettingsRequest} msg The message object to deserialize into. + * @param {!jspb.BinaryReader} reader The BinaryReader to use. + * @return {!proto.settings.v1.GetSettingsRequest} + */ +proto.settings.v1.GetSettingsRequest.deserializeBinaryFromReader = function(msg, reader) { + while (reader.nextField()) { + if (reader.isEndGroup()) { + break; + } + var field = reader.getFieldNumber(); + switch (field) { + default: + reader.skipField(); + break; + } + } + return msg; +}; + + +/** + * Serializes the message to binary data (in protobuf wire format). + * @return {!Uint8Array} + */ +proto.settings.v1.GetSettingsRequest.prototype.serializeBinary = function() { + var writer = new jspb.BinaryWriter(); + proto.settings.v1.GetSettingsRequest.serializeBinaryToWriter(this, writer); + return writer.getResultBuffer(); +}; + + +/** + * Serializes the given message to binary data (in protobuf wire + * format), writing to the given BinaryWriter. + * @param {!proto.settings.v1.GetSettingsRequest} message + * @param {!jspb.BinaryWriter} writer + * @suppress {unusedLocalVariables} f is only used for nested messages + */ +proto.settings.v1.GetSettingsRequest.serializeBinaryToWriter = function(message, writer) { + var f = undefined; +}; + + + +/** + * List of repeated fields within this message type. + * @private {!Array} + * @const + */ +proto.settings.v1.GetSettingsResponse.repeatedFields_ = [1]; + + + +if (jspb.Message.GENERATE_TO_OBJECT) { +/** + * Creates an object representation of this proto. + * Field names that are reserved in JavaScript and will be renamed to pb_name. + * Optional fields that are not set will be set to undefined. + * To access a reserved field use, foo.pb_, eg, foo.pb_default. + * For the list of reserved names please see: + * net/proto2/compiler/js/internal/generator.cc#kKeyword. + * @param {boolean=} opt_includeInstance Deprecated. whether to include the + * JSPB instance for transitional soy proto support: + * http://goto/soy-param-migration + * @return {!Object} + */ +proto.settings.v1.GetSettingsResponse.prototype.toObject = function(opt_includeInstance) { + return proto.settings.v1.GetSettingsResponse.toObject(opt_includeInstance, this); +}; + + +/** + * Static version of the {@see toObject} method. + * @param {boolean|undefined} includeInstance Deprecated. Whether to include + * the JSPB instance for transitional soy proto support: + * http://goto/soy-param-migration + * @param {!proto.settings.v1.GetSettingsResponse} msg The msg instance to transform. + * @return {!Object} + * @suppress {unusedLocalVariables} f is only used for nested messages + */ +proto.settings.v1.GetSettingsResponse.toObject = function(includeInstance, msg) { + var f, obj = { + settingsList: jspb.Message.toObjectList(msg.getSettingsList(), + proto.settings.v1.Setting.toObject, includeInstance) + }; + + if (includeInstance) { + obj.$jspbMessageInstance = msg; + } + return obj; +}; +} + + +/** + * Deserializes binary data (in protobuf wire format). + * @param {jspb.ByteSource} bytes The bytes to deserialize. + * @return {!proto.settings.v1.GetSettingsResponse} + */ +proto.settings.v1.GetSettingsResponse.deserializeBinary = function(bytes) { + var reader = new jspb.BinaryReader(bytes); + var msg = new proto.settings.v1.GetSettingsResponse; + return proto.settings.v1.GetSettingsResponse.deserializeBinaryFromReader(msg, reader); +}; + + +/** + * Deserializes binary data (in protobuf wire format) from the + * given reader into the given message object. + * @param {!proto.settings.v1.GetSettingsResponse} msg The message object to deserialize into. + * @param {!jspb.BinaryReader} reader The BinaryReader to use. + * @return {!proto.settings.v1.GetSettingsResponse} + */ +proto.settings.v1.GetSettingsResponse.deserializeBinaryFromReader = function(msg, reader) { + while (reader.nextField()) { + if (reader.isEndGroup()) { + break; + } + var field = reader.getFieldNumber(); + switch (field) { + case 1: + var value = new proto.settings.v1.Setting; + reader.readMessage(value,proto.settings.v1.Setting.deserializeBinaryFromReader); + msg.addSettings(value); + break; + default: + reader.skipField(); + break; + } + } + return msg; +}; + + +/** + * Serializes the message to binary data (in protobuf wire format). + * @return {!Uint8Array} + */ +proto.settings.v1.GetSettingsResponse.prototype.serializeBinary = function() { + var writer = new jspb.BinaryWriter(); + proto.settings.v1.GetSettingsResponse.serializeBinaryToWriter(this, writer); + return writer.getResultBuffer(); +}; + + +/** + * Serializes the given message to binary data (in protobuf wire + * format), writing to the given BinaryWriter. + * @param {!proto.settings.v1.GetSettingsResponse} message + * @param {!jspb.BinaryWriter} writer + * @suppress {unusedLocalVariables} f is only used for nested messages + */ +proto.settings.v1.GetSettingsResponse.serializeBinaryToWriter = function(message, writer) { + var f = undefined; + f = message.getSettingsList(); + if (f.length > 0) { + writer.writeRepeatedMessage( + 1, + f, + proto.settings.v1.Setting.serializeBinaryToWriter + ); + } +}; + + +/** + * repeated Setting settings = 1; + * @return {!Array} + */ +proto.settings.v1.GetSettingsResponse.prototype.getSettingsList = function() { + return /** @type{!Array} */ ( + jspb.Message.getRepeatedWrapperField(this, proto.settings.v1.Setting, 1)); +}; + + +/** + * @param {!Array} value + * @return {!proto.settings.v1.GetSettingsResponse} returns this +*/ +proto.settings.v1.GetSettingsResponse.prototype.setSettingsList = function(value) { + return jspb.Message.setRepeatedWrapperField(this, 1, value); +}; + + +/** + * @param {!proto.settings.v1.Setting=} opt_value + * @param {number=} opt_index + * @return {!proto.settings.v1.Setting} + */ +proto.settings.v1.GetSettingsResponse.prototype.addSettings = function(opt_value, opt_index) { + return jspb.Message.addToRepeatedWrapperField(this, 1, opt_value, proto.settings.v1.Setting, opt_index); +}; + + +/** + * Clears the list making it empty but non-null. + * @return {!proto.settings.v1.GetSettingsResponse} returns this + */ +proto.settings.v1.GetSettingsResponse.prototype.clearSettingsList = function() { + return this.setSettingsList([]); +}; + + + + + +if (jspb.Message.GENERATE_TO_OBJECT) { +/** + * Creates an object representation of this proto. + * Field names that are reserved in JavaScript and will be renamed to pb_name. + * Optional fields that are not set will be set to undefined. + * To access a reserved field use, foo.pb_, eg, foo.pb_default. + * For the list of reserved names please see: + * net/proto2/compiler/js/internal/generator.cc#kKeyword. + * @param {boolean=} opt_includeInstance Deprecated. whether to include the + * JSPB instance for transitional soy proto support: + * http://goto/soy-param-migration + * @return {!Object} + */ +proto.settings.v1.SetSettingsRequest.prototype.toObject = function(opt_includeInstance) { + return proto.settings.v1.SetSettingsRequest.toObject(opt_includeInstance, this); +}; + + +/** + * Static version of the {@see toObject} method. + * @param {boolean|undefined} includeInstance Deprecated. Whether to include + * the JSPB instance for transitional soy proto support: + * http://goto/soy-param-migration + * @param {!proto.settings.v1.SetSettingsRequest} msg The msg instance to transform. + * @return {!Object} + * @suppress {unusedLocalVariables} f is only used for nested messages + */ +proto.settings.v1.SetSettingsRequest.toObject = function(includeInstance, msg) { + var f, obj = { + setting: (f = msg.getSetting()) && proto.settings.v1.Setting.toObject(includeInstance, f) + }; + + if (includeInstance) { + obj.$jspbMessageInstance = msg; + } + return obj; +}; +} + + +/** + * Deserializes binary data (in protobuf wire format). + * @param {jspb.ByteSource} bytes The bytes to deserialize. + * @return {!proto.settings.v1.SetSettingsRequest} + */ +proto.settings.v1.SetSettingsRequest.deserializeBinary = function(bytes) { + var reader = new jspb.BinaryReader(bytes); + var msg = new proto.settings.v1.SetSettingsRequest; + return proto.settings.v1.SetSettingsRequest.deserializeBinaryFromReader(msg, reader); +}; + + +/** + * Deserializes binary data (in protobuf wire format) from the + * given reader into the given message object. + * @param {!proto.settings.v1.SetSettingsRequest} msg The message object to deserialize into. + * @param {!jspb.BinaryReader} reader The BinaryReader to use. + * @return {!proto.settings.v1.SetSettingsRequest} + */ +proto.settings.v1.SetSettingsRequest.deserializeBinaryFromReader = function(msg, reader) { + while (reader.nextField()) { + if (reader.isEndGroup()) { + break; + } + var field = reader.getFieldNumber(); + switch (field) { + case 1: + var value = new proto.settings.v1.Setting; + reader.readMessage(value,proto.settings.v1.Setting.deserializeBinaryFromReader); + msg.setSetting(value); + break; + default: + reader.skipField(); + break; + } + } + return msg; +}; + + +/** + * Serializes the message to binary data (in protobuf wire format). + * @return {!Uint8Array} + */ +proto.settings.v1.SetSettingsRequest.prototype.serializeBinary = function() { + var writer = new jspb.BinaryWriter(); + proto.settings.v1.SetSettingsRequest.serializeBinaryToWriter(this, writer); + return writer.getResultBuffer(); +}; + + +/** + * Serializes the given message to binary data (in protobuf wire + * format), writing to the given BinaryWriter. + * @param {!proto.settings.v1.SetSettingsRequest} message + * @param {!jspb.BinaryWriter} writer + * @suppress {unusedLocalVariables} f is only used for nested messages + */ +proto.settings.v1.SetSettingsRequest.serializeBinaryToWriter = function(message, writer) { + var f = undefined; + f = message.getSetting(); + if (f != null) { + writer.writeMessage( + 1, + f, + proto.settings.v1.Setting.serializeBinaryToWriter + ); + } +}; + + +/** + * optional Setting setting = 1; + * @return {?proto.settings.v1.Setting} + */ +proto.settings.v1.SetSettingsRequest.prototype.getSetting = function() { + return /** @type{?proto.settings.v1.Setting} */ ( + jspb.Message.getWrapperField(this, proto.settings.v1.Setting, 1)); +}; + + +/** + * @param {?proto.settings.v1.Setting|undefined} value + * @return {!proto.settings.v1.SetSettingsRequest} returns this +*/ +proto.settings.v1.SetSettingsRequest.prototype.setSetting = function(value) { + return jspb.Message.setWrapperField(this, 1, value); +}; + + +/** + * Clears the message field making it undefined. + * @return {!proto.settings.v1.SetSettingsRequest} returns this + */ +proto.settings.v1.SetSettingsRequest.prototype.clearSetting = function() { + return this.setSetting(undefined); +}; + + +/** + * Returns whether this field is set. + * @return {boolean} + */ +proto.settings.v1.SetSettingsRequest.prototype.hasSetting = function() { + return jspb.Message.getField(this, 1) != null; +}; + + + + + +if (jspb.Message.GENERATE_TO_OBJECT) { +/** + * Creates an object representation of this proto. + * Field names that are reserved in JavaScript and will be renamed to pb_name. + * Optional fields that are not set will be set to undefined. + * To access a reserved field use, foo.pb_, eg, foo.pb_default. + * For the list of reserved names please see: + * net/proto2/compiler/js/internal/generator.cc#kKeyword. + * @param {boolean=} opt_includeInstance Deprecated. whether to include the + * JSPB instance for transitional soy proto support: + * http://goto/soy-param-migration + * @return {!Object} + */ +proto.settings.v1.SetSettingsResponse.prototype.toObject = function(opt_includeInstance) { + return proto.settings.v1.SetSettingsResponse.toObject(opt_includeInstance, this); +}; + + +/** + * Static version of the {@see toObject} method. + * @param {boolean|undefined} includeInstance Deprecated. Whether to include + * the JSPB instance for transitional soy proto support: + * http://goto/soy-param-migration + * @param {!proto.settings.v1.SetSettingsResponse} msg The msg instance to transform. + * @return {!Object} + * @suppress {unusedLocalVariables} f is only used for nested messages + */ +proto.settings.v1.SetSettingsResponse.toObject = function(includeInstance, msg) { + var f, obj = { + setting: (f = msg.getSetting()) && proto.settings.v1.Setting.toObject(includeInstance, f) + }; + + if (includeInstance) { + obj.$jspbMessageInstance = msg; + } + return obj; +}; +} + + +/** + * Deserializes binary data (in protobuf wire format). + * @param {jspb.ByteSource} bytes The bytes to deserialize. + * @return {!proto.settings.v1.SetSettingsResponse} + */ +proto.settings.v1.SetSettingsResponse.deserializeBinary = function(bytes) { + var reader = new jspb.BinaryReader(bytes); + var msg = new proto.settings.v1.SetSettingsResponse; + return proto.settings.v1.SetSettingsResponse.deserializeBinaryFromReader(msg, reader); +}; + + +/** + * Deserializes binary data (in protobuf wire format) from the + * given reader into the given message object. + * @param {!proto.settings.v1.SetSettingsResponse} msg The message object to deserialize into. + * @param {!jspb.BinaryReader} reader The BinaryReader to use. + * @return {!proto.settings.v1.SetSettingsResponse} + */ +proto.settings.v1.SetSettingsResponse.deserializeBinaryFromReader = function(msg, reader) { + while (reader.nextField()) { + if (reader.isEndGroup()) { + break; + } + var field = reader.getFieldNumber(); + switch (field) { + case 1: + var value = new proto.settings.v1.Setting; + reader.readMessage(value,proto.settings.v1.Setting.deserializeBinaryFromReader); + msg.setSetting(value); + break; + default: + reader.skipField(); + break; + } + } + return msg; +}; + + +/** + * Serializes the message to binary data (in protobuf wire format). + * @return {!Uint8Array} + */ +proto.settings.v1.SetSettingsResponse.prototype.serializeBinary = function() { + var writer = new jspb.BinaryWriter(); + proto.settings.v1.SetSettingsResponse.serializeBinaryToWriter(this, writer); + return writer.getResultBuffer(); +}; + + +/** + * Serializes the given message to binary data (in protobuf wire + * format), writing to the given BinaryWriter. + * @param {!proto.settings.v1.SetSettingsResponse} message + * @param {!jspb.BinaryWriter} writer + * @suppress {unusedLocalVariables} f is only used for nested messages + */ +proto.settings.v1.SetSettingsResponse.serializeBinaryToWriter = function(message, writer) { + var f = undefined; + f = message.getSetting(); + if (f != null) { + writer.writeMessage( + 1, + f, + proto.settings.v1.Setting.serializeBinaryToWriter + ); + } +}; + + +/** + * optional Setting setting = 1; + * @return {?proto.settings.v1.Setting} + */ +proto.settings.v1.SetSettingsResponse.prototype.getSetting = function() { + return /** @type{?proto.settings.v1.Setting} */ ( + jspb.Message.getWrapperField(this, proto.settings.v1.Setting, 1)); +}; + + +/** + * @param {?proto.settings.v1.Setting|undefined} value + * @return {!proto.settings.v1.SetSettingsResponse} returns this +*/ +proto.settings.v1.SetSettingsResponse.prototype.setSetting = function(value) { + return jspb.Message.setWrapperField(this, 1, value); +}; + + +/** + * Clears the message field making it undefined. + * @return {!proto.settings.v1.SetSettingsResponse} returns this + */ +proto.settings.v1.SetSettingsResponse.prototype.clearSetting = function() { + return this.setSetting(undefined); +}; + + +/** + * Returns whether this field is set. + * @return {boolean} + */ +proto.settings.v1.SetSettingsResponse.prototype.hasSetting = function() { + return jspb.Message.getField(this, 1) != null; +}; + + + + + +if (jspb.Message.GENERATE_TO_OBJECT) { +/** + * Creates an object representation of this proto. + * Field names that are reserved in JavaScript and will be renamed to pb_name. + * Optional fields that are not set will be set to undefined. + * To access a reserved field use, foo.pb_, eg, foo.pb_default. + * For the list of reserved names please see: + * net/proto2/compiler/js/internal/generator.cc#kKeyword. + * @param {boolean=} opt_includeInstance Deprecated. whether to include the + * JSPB instance for transitional soy proto support: + * http://goto/soy-param-migration + * @return {!Object} + */ +proto.settings.v1.Setting.prototype.toObject = function(opt_includeInstance) { + return proto.settings.v1.Setting.toObject(opt_includeInstance, this); +}; + + +/** + * Static version of the {@see toObject} method. + * @param {boolean|undefined} includeInstance Deprecated. Whether to include + * the JSPB instance for transitional soy proto support: + * http://goto/soy-param-migration + * @param {!proto.settings.v1.Setting} msg The msg instance to transform. + * @return {!Object} + * @suppress {unusedLocalVariables} f is only used for nested messages + */ +proto.settings.v1.Setting.toObject = function(includeInstance, msg) { + var f, obj = { + name: jspb.Message.getFieldWithDefault(msg, 1, ""), + value: jspb.Message.getFieldWithDefault(msg, 2, ""), + modifiedat: jspb.Message.getFieldWithDefault(msg, 3, 0) + }; + + if (includeInstance) { + obj.$jspbMessageInstance = msg; + } + return obj; +}; +} + + +/** + * Deserializes binary data (in protobuf wire format). + * @param {jspb.ByteSource} bytes The bytes to deserialize. + * @return {!proto.settings.v1.Setting} + */ +proto.settings.v1.Setting.deserializeBinary = function(bytes) { + var reader = new jspb.BinaryReader(bytes); + var msg = new proto.settings.v1.Setting; + return proto.settings.v1.Setting.deserializeBinaryFromReader(msg, reader); +}; + + +/** + * Deserializes binary data (in protobuf wire format) from the + * given reader into the given message object. + * @param {!proto.settings.v1.Setting} msg The message object to deserialize into. + * @param {!jspb.BinaryReader} reader The BinaryReader to use. + * @return {!proto.settings.v1.Setting} + */ +proto.settings.v1.Setting.deserializeBinaryFromReader = function(msg, reader) { + while (reader.nextField()) { + if (reader.isEndGroup()) { + break; + } + var field = reader.getFieldNumber(); + switch (field) { + case 1: + var value = /** @type {string} */ (reader.readString()); + msg.setName(value); + break; + case 2: + var value = /** @type {string} */ (reader.readString()); + msg.setValue(value); + break; + case 3: + var value = /** @type {number} */ (reader.readInt64()); + msg.setModifiedat(value); + break; + default: + reader.skipField(); + break; + } + } + return msg; +}; + + +/** + * Serializes the message to binary data (in protobuf wire format). + * @return {!Uint8Array} + */ +proto.settings.v1.Setting.prototype.serializeBinary = function() { + var writer = new jspb.BinaryWriter(); + proto.settings.v1.Setting.serializeBinaryToWriter(this, writer); + return writer.getResultBuffer(); +}; + + +/** + * Serializes the given message to binary data (in protobuf wire + * format), writing to the given BinaryWriter. + * @param {!proto.settings.v1.Setting} message + * @param {!jspb.BinaryWriter} writer + * @suppress {unusedLocalVariables} f is only used for nested messages + */ +proto.settings.v1.Setting.serializeBinaryToWriter = function(message, writer) { + var f = undefined; + f = message.getName(); + if (f.length > 0) { + writer.writeString( + 1, + f + ); + } + f = message.getValue(); + if (f.length > 0) { + writer.writeString( + 2, + f + ); + } + f = message.getModifiedat(); + if (f !== 0) { + writer.writeInt64( + 3, + f + ); + } +}; + + +/** + * optional string name = 1; + * @return {string} + */ +proto.settings.v1.Setting.prototype.getName = function() { + return /** @type {string} */ (jspb.Message.getFieldWithDefault(this, 1, "")); +}; + + +/** + * @param {string} value + * @return {!proto.settings.v1.Setting} returns this + */ +proto.settings.v1.Setting.prototype.setName = function(value) { + return jspb.Message.setProto3StringField(this, 1, value); +}; + + +/** + * optional string value = 2; + * @return {string} + */ +proto.settings.v1.Setting.prototype.getValue = function() { + return /** @type {string} */ (jspb.Message.getFieldWithDefault(this, 2, "")); +}; + + +/** + * @param {string} value + * @return {!proto.settings.v1.Setting} returns this + */ +proto.settings.v1.Setting.prototype.setValue = function(value) { + return jspb.Message.setProto3StringField(this, 2, value); +}; + + +/** + * optional int64 modifiedAt = 3; + * @return {number} + */ +proto.settings.v1.Setting.prototype.getModifiedat = function() { + return /** @type {number} */ (jspb.Message.getFieldWithDefault(this, 3, 0)); +}; + + +/** + * @param {number} value + * @return {!proto.settings.v1.Setting} returns this + */ +proto.settings.v1.Setting.prototype.setModifiedat = function(value) { + return jspb.Message.setProto3IntField(this, 3, value); +}; + + +goog.object.extend(exports, proto.settings.v1); diff --git a/pyroscope/shared.js b/pyroscope/shared.js index c954fb3f..2a37005a 100644 --- a/pyroscope/shared.js +++ b/pyroscope/shared.js @@ -1,3 +1,6 @@ +const { QrynBadRequest } = require('../lib/handlers/errors') +const Sql = require('@cloki/clickhouse-sql') +const compiler = require('../parser/bnf') /** * * @param payload {ReadableStream} @@ -22,6 +25,159 @@ const bufferize = async (payload) => { return body } +const parser = (MsgClass) => { + return async (req, payload) => { + const body = await bufferize(payload) + req._rawBody = body + return MsgClass.deserializeBinary(body) + } +} + +/** + * + * @param proto {Object} + */ +const normalizeProtoResponse = (proto) => { + if (typeof proto !== 'object') { + return proto + } + return Object.fromEntries(Object.entries(proto).map((e) => { + let name = e[0] + if (name.endsWith('List')) { + name = name.slice(0, -4) + } + if (Array.isArray(e[1])) { + return [name, e[1].map(normalizeProtoResponse)] + } + if (typeof e[1] === 'object') { + return [name, normalizeProtoResponse(e[1])] + } + return [name, e[1]] + })) +} + +const wrapResponse = (hndl) => { + return async (req, res) => { + const _res = await hndl(req, res) + if (!_res || !_res.serializeBinary) { + return _res + } + if (req.type === 'json') { + const strRes = JSON.stringify(normalizeProtoResponse(_res.toObject())) + return res.code(200).send(strRes) + } + return res.code(200).send(Buffer.from(_res.serializeBinary())) + } +} + +const serviceNameSelectorQuery = (labelSelector) => { + const empty = Sql.Eq(new Sql.Raw('1'), new Sql.Raw('1')) + if (!labelSelector || !labelSelector.length || labelSelector === '{}') { + return empty + } + const labelSelectorScript = parseLabelSelector(labelSelector) + let conds = null + for (const rule of labelSelectorScript.Children('log_stream_selector_rule')) { + const label = rule.Child('label').value + if (label !== 'service_name') { + continue + } + const val = JSON.parse(rule.Child('quoted_str').value) + let valRul = null + switch (rule.Child('operator').value) { + case '=': + valRul = Sql.Eq(new Sql.Raw('service_name'), Sql.val(val)) + break + case '!=': + valRul = Sql.Ne(new Sql.Raw('service_name'), Sql.val(val)) + break + case '=~': + valRul = Sql.Eq(new Sql.Raw(`match(service_name, ${Sql.quoteVal(val)})`), 1) + break + case '!~': + valRul = Sql.Ne(new Sql.Raw(`match(service_name, ${Sql.quoteVal(val)})`), 1) + } + conds = valRul + } + return conds || empty +} + +const parseLabelSelector = (labelSelector) => { + if (labelSelector.endsWith(',}')) { + labelSelector = labelSelector.slice(0, -2) + '}' + } + return compiler.ParseScript(labelSelector).rootToken +} + +/** + * + * @param typeId {string} + */ +const parseTypeId = (typeId) => { + const typeParts = typeId.match(/^([^:]+):([^:]+):([^:]+):([^:]+):([^:]+)$/) + if (!typeParts) { + throw new QrynBadRequest('invalid type id') + } + return { + type: typeParts[1], + sampleType: typeParts[2], + sampleUnit: typeParts[3], + periodType: typeParts[4], + periodUnit: typeParts[5] + } +} + + +/** + * + * @param {Sql.Select} query + * @param {string} labelSelector + */ +const labelSelectorQuery = (query, labelSelector) => { + if (!labelSelector || !labelSelector.length || labelSelector === '{}') { + return query + } + const labelSelectorScript = parseLabelSelector(labelSelector) + const labelsConds = [] + for (const rule of labelSelectorScript.Children('log_stream_selector_rule')) { + const val = JSON.parse(rule.Child('quoted_str').value) + let valRul = null + switch (rule.Child('operator').value) { + case '=': + valRul = Sql.Eq(new Sql.Raw('val'), Sql.val(val)) + break + case '!=': + valRul = Sql.Ne(new Sql.Raw('val'), Sql.val(val)) + break + case '=~': + valRul = Sql.Eq(new Sql.Raw(`match(val, ${Sql.quoteVal(val)})`), 1) + break + case '!~': + valRul = Sql.Ne(new Sql.Raw(`match(val, ${Sql.quoteVal(val)})`), 1) + } + const labelSubCond = Sql.And( + Sql.Eq('key', Sql.val(rule.Child('label').value)), + valRul + ) + labelsConds.push(labelSubCond) + } + query.where(Sql.Or(...labelsConds)) + query.groupBy(new Sql.Raw('fingerprint')) + query.having(Sql.Eq( + new Sql.Raw(`groupBitOr(${labelsConds.map((cond, i) => { + return `bitShiftLeft(toUInt64(${cond}), ${i})` + }).join('+')})`), + new Sql.Raw(`bitShiftLeft(toUInt64(1), ${labelsConds.length})-1`) + )) +} + module.exports = { - bufferize + bufferize, + parser, + normalizeProtoResponse, + wrapResponse, + parseTypeId, + serviceNameSelectorQuery, + parseLabelSelector, + labelSelectorQuery } From decb26cb5e8a23518a172da8a5fe405292577fee Mon Sep 17 00:00:00 2001 From: akvlad Date: Mon, 5 Aug 2024 17:49:53 +0300 Subject: [PATCH 3/8] render endpoint impl --- pyroscope/flamebearer.d.ts | 3 +- pyroscope/pyroscope.js | 138 ++------------------- pyroscope/render.js | 243 +++++++++++++++++++++++++++---------- pyroscope/select_series.js | 142 ++++++++++++++++++++++ pyroscope/shared.js | 5 +- 5 files changed, 337 insertions(+), 194 deletions(-) create mode 100644 pyroscope/select_series.js diff --git a/pyroscope/flamebearer.d.ts b/pyroscope/flamebearer.d.ts index a62150ea..27a701dd 100644 --- a/pyroscope/flamebearer.d.ts +++ b/pyroscope/flamebearer.d.ts @@ -6,13 +6,14 @@ type units = string; export interface Flamebearer { version: number, flamebearerProfileV1: flamebearerProfileV1 - telemetry: {[key: string]: any} + telemetry?: {[key: string]: any} } export interface flamebearerProfileV1 { flamebearer: flamebearerV1, metadata: flamebearerMetadataV1, timeline: flamebearerTimelineV1, + groups: {[key: string]: flamebearerTimelineV1} heatmap: heatmap, leftTicks: string, rightTicks: string, diff --git a/pyroscope/pyroscope.js b/pyroscope/pyroscope.js index f8976192..61ea2a48 100644 --- a/pyroscope/pyroscope.js +++ b/pyroscope/pyroscope.js @@ -9,10 +9,17 @@ const { QrynBadRequest } = require('../lib/handlers/errors') const { clusterName } = require('../common') const logger = require('../lib/logger') const jsonParsers = require('./json_parsers') -const { parser, wrapResponse, parseTypeId, serviceNameSelectorQuery, labelSelectorQuery } = require('./shared') -const HISTORY_TIMESPAN = 1000 * 60 * 60 * 24 * 7 +const { + parser, + wrapResponse, + parseTypeId, + serviceNameSelectorQuery, + labelSelectorQuery, + HISTORY_TIMESPAN +} = require('./shared') const settings = require('./settings') const { mergeStackTraces } = require('./merge_stack_traces') +const { selectSeriesImpl } = require('./select_series') const render = require('./render') const profileTypesHandler = async (req, res) => { @@ -103,138 +110,13 @@ const selectMergeStacktracesV2 = async (req, res) => { } const selectSeries = async (req, res) => { - const _req = req.body const fromTimeSec = Math.floor(req.getStart && req.getStart() ? parseInt(req.getStart()) / 1000 : Date.now() / 1000 - HISTORY_TIMESPAN) const toTimeSec = Math.floor(req.getEnd && req.getEnd() ? parseInt(req.getEnd()) / 1000 : Date.now() / 1000) - let typeID = _req.getProfileTypeid && _req.getProfileTypeid() - if (!typeID) { - throw new QrynBadRequest('No type provided') - } - typeID = parseTypeId(typeID) - if (!typeID) { - throw new QrynBadRequest('Invalid type provided') - } - const dist = clusterName ? '_dist' : '' - const sampleTypeId = typeID.sampleType + ':' + typeID.sampleUnit - const labelSelector = _req.getLabelSelector && _req.getLabelSelector() - let groupBy = _req.getGroupByList && _req.getGroupByList() - groupBy = groupBy && groupBy.length ? groupBy : null - const step = _req.getStep && parseInt(_req.getStep()) - if (!step || isNaN(step)) { - throw new QrynBadRequest('No step provided') - } - const aggregation = _req.getAggregation && _req.getAggregation() - - const typeIdSelector = Sql.Eq( - 'type_id', - Sql.val(`${typeID.type}:${typeID.periodType}:${typeID.periodUnit}`)) - const serviceNameSelector = serviceNameSelectorQuery(labelSelector) - - const idxReq = (new Sql.Select()) - .select(new Sql.Raw('fingerprint')) - .from(`${DATABASE_NAME()}.profiles_series_gin`) - .where( - Sql.And( - typeIdSelector, - serviceNameSelector, - Sql.Gte('date', new Sql.Raw(`toDate(FROM_UNIXTIME(${Math.floor(fromTimeSec)}))`)), - Sql.Lte('date', new Sql.Raw(`toDate(FROM_UNIXTIME(${Math.floor(toTimeSec)}))`)), - Sql.Eq(new Sql.Raw( - `has(sample_types_units, (${Sql.quoteVal(typeID.sampleType)}, ${Sql.quoteVal(typeID.sampleUnit)}))`), - 1) - ) - ) - labelSelectorQuery(idxReq, labelSelector) - - const withIdxReq = (new Sql.With('idx', idxReq, !!clusterName)) - - let tagsReq = 'arraySort(p.tags)' - if (groupBy) { - tagsReq = `arraySort(arrayFilter(x -> x.1 in (${groupBy.map(g => Sql.quoteVal(g)).join(',')}), p.tags))` - } - - const labelsReq = (new Sql.Select()).with(withIdxReq).select( - 'fingerprint', - [new Sql.Raw(tagsReq), 'tags'], - [groupBy ? new Sql.Raw('cityHash64(tags)') : 'fingerprint', 'new_fingerprint'] - ).distinct(true).from([`${DATABASE_NAME()}.profiles_series`, 'p']) - .where(Sql.And( - new Sql.In('fingerprint', 'IN', new Sql.WithReference(withIdxReq)), - Sql.Gte('date', new Sql.Raw(`toDate(FROM_UNIXTIME(${Math.floor(fromTimeSec)}))`)), - Sql.Lte('date', new Sql.Raw(`toDate(FROM_UNIXTIME(${Math.floor(toTimeSec)}))`)), - typeIdSelector, - serviceNameSelector - )) - - const withLabelsReq = new Sql.With('labels', labelsReq, !!clusterName) - - let valueCol = new Sql.Raw( - `sum(toFloat64(arrayFirst(x -> x.1 == ${Sql.quoteVal(sampleTypeId)}, p.values_agg).2))`) - if (aggregation === types.TimeSeriesAggregationType.TIME_SERIES_AGGREGATION_TYPE_AVERAGE) { - valueCol = new Sql.Raw( - `sum(toFloat64(arrayFirst(x -> x.1 == ${Sql.quoteVal(sampleTypeId)}).2, p.values_agg)) / ` + - `sum(toFloat64(arrayFirst(x -> x.1 == ${Sql.quoteVal(sampleTypeId)}).3, p.values_agg))` - ) - } - - const mainReq = (new Sql.Select()).with(withIdxReq, withLabelsReq).select( - [new Sql.Raw(`intDiv(p.timestamp_ns, 1000000000 * ${step}) * ${step} * 1000`), 'timestamp_ms'], - [new Sql.Raw('labels.new_fingerprint'), 'fingerprint'], - [new Sql.Raw('min(labels.tags)'), 'labels'], - [valueCol, 'value'] - ).from([`${DATABASE_NAME()}.profiles${dist}`, 'p']).join( - [new Sql.WithReference(withLabelsReq), 'labels'], - 'ANY LEFT', - Sql.Eq(new Sql.Raw('p.fingerprint'), new Sql.Raw('labels.fingerprint')) - ).where( - Sql.And( - new Sql.In('p.fingerprint', 'IN', new Sql.WithReference(withIdxReq)), - Sql.Gte('p.timestamp_ns', new Sql.Raw(`${fromTimeSec}000000000`)), - Sql.Lt('p.timestamp_ns', new Sql.Raw(`${toTimeSec}000000000`)), - typeIdSelector, - serviceNameSelector - ) - ).groupBy('timestamp_ns', 'fingerprint') - .orderBy(['fingerprint', 'ASC'], ['timestamp_ns', 'ASC']) - const strMainReq = mainReq.toString() - const chRes = await clickhouse - .rawRequest(strMainReq + ' FORMAT JSON', null, DATABASE_NAME()) - - let lastFingerprint = null - const seriesList = [] - let lastSeries = null - let lastPoints = [] - for (let i = 0; i < chRes.data.data.length; i++) { - const e = chRes.data.data[i] - if (lastFingerprint !== e.fingerprint) { - lastFingerprint = e.fingerprint - lastSeries && lastSeries.setPointsList(lastPoints) - lastSeries && seriesList.push(lastSeries) - lastPoints = [] - lastSeries = new types.Series() - lastSeries.setLabelsList(e.labels.map(l => { - const lp = new types.LabelPair() - lp.setName(l[0]) - lp.setValue(l[1]) - return lp - })) - } - - const p = new types.Point() - p.setValue(e.value) - p.setTimestamp(e.timestamp_ms) - lastPoints.push(p) - } - lastSeries && lastSeries.setPointsList(lastPoints) - lastSeries && seriesList.push(lastSeries) - - const resp = new messages.SelectSeriesResponse() - resp.setSeriesList(seriesList) - return resp + return selectSeriesImpl(fromTimeSec, toTimeSec, req.body) } const selectMergeProfile = async (req, res) => { diff --git a/pyroscope/render.js b/pyroscope/render.js index 1d93e3e8..ce5362c7 100644 --- a/pyroscope/render.js +++ b/pyroscope/render.js @@ -1,5 +1,8 @@ const { parseTypeId } = require('./shared') const { mergeStackTraces } = require('./merge_stack_traces') +const querierMessages = require('./querier_pb') +const { selectSeriesImpl } = require('./select_series') +const types = require('./types/v1/types_pb') const render = async (req, res) => { const query = req.query.query @@ -8,12 +11,12 @@ const render = async (req, res) => { ? Math.floor(parseInt(req.query.from) / 1000) : Math.floor((Date.now() - 1000 * 60 * 60 * 48) / 1000) const toTimeSec = req.query.until - ? Math.floor(parseInt(req.query.from) / 1000) + ? Math.floor(parseInt(req.query.until) / 1000) : Math.floor((Date.now() - 1000 * 60 * 60 * 48) / 1000) if (!parsedQuery) { return res.sendStatus(400).send('Invalid query') } - const groupBy = req.query.groupBy + const groupBy = req.query.groupBy || [] let agg = '' switch (req.query.aggregation) { case 'sum': @@ -26,80 +29,184 @@ const render = async (req, res) => { if (req.query.format === 'dot') { return res.sendStatus(400).send('Dot format is not supported') } - const mergeStackTrace = mergeStackTraces( + const promises = [] + promises.push(mergeStackTraces( parsedQuery.typeDesc, - parsedQuery.labelSelector, + '{' + parsedQuery.labelSelector + '}', fromTimeSec, toTimeSec, - req.log) - //TODO -} + req.log)) -/* -func (q *QueryHandlers) Render(w http.ResponseWriter, req *http.Request) { - var resFlame *connect.Response[querierv1.SelectMergeStacktracesResponse] - g, ctx := errgroup.WithContext(req.Context()) - selectParamsClone := selectParams.CloneVT() - g.Go(func() error { - var err error - resFlame, err = q.client.SelectMergeStacktraces(ctx, connect.NewRequest(selectParamsClone)) - return err - }) + const timelineStep = calcIntervalSec(fromTimeSec, toTimeSec) + promises.push(selectSeriesImpl( + fromTimeSec, + toTimeSec, + { + getProfileTypeid: () => parsedQuery.typeId, + getLabelSelector: () => `{${parsedQuery.labelSelector}}`, + getGroupByList: () => groupBy, + getStep: () => timelineStep, + getAggregation: () => agg + } + )) + const [bMergeStackTrace, selectSeries] = + await Promise.all(promises) + const mergeStackTrace = querierMessages.SelectMergeStacktracesResponse.deserializeBinary(bMergeStackTrace) + let series = new types.Series() + if (selectSeries.getSeriesList().length === 1) { + series = selectSeries.getSeriesList()[0] + } + const fb = toFlamebearer(mergeStackTrace.getFlamegraph(), parsedQuery.profileType) + fb.flamebearerProfileV1.timeline = timeline(series, + fromTimeSec * 1000, + toTimeSec * 1000, + timelineStep) - timelineStep := timeline.CalcPointInterval(selectParams.Start, selectParams.End) - var resSeries *connect.Response[querierv1.SelectSeriesResponse] - g.Go(func() error { - var err error - resSeries, err = q.client.SelectSeries(req.Context(), - connect.NewRequest(&querierv1.SelectSeriesRequest{ - ProfileTypeID: selectParams.ProfileTypeID, - LabelSelector: selectParams.LabelSelector, - Start: selectParams.Start, - End: selectParams.End, - Step: timelineStep, - GroupBy: groupBy, - Aggregation: &aggregation, - })) + if (groupBy.length > 0) { + fb.flamebearerProfileV1.groups = {} + let key = '*' + series.getSeriesList().forEach((_series) => { + _series.getLabelsList().forEach((label) => { + key = label.getName() === groupBy[0] ? label.getValue() : key + }) + }) + fb.flamebearerProfileV1.groups[key] = timeline(series, + fromTimeSec * 1000, + toTimeSec * 1000, + timelineStep) + } + res.code(200) + res.headers({ 'Content-Type': 'application/json' }) + return res.send(Buffer.from(JSON.stringify(fb.flamebearerProfileV1))) +} - return err - }) +/** + * + * @param fg + * @param profileType + * @returns {Flamebearer} + */ +function toFlamebearer (fg, profileType) { + if (!fg) { + fg = new querierMessages.FlameGraph() + } + let unit = profileType.getSampleUnit() + let sampleRate = 100 + switch (profileType.getSampleType()) { + case 'inuse_objects': + case 'alloc_objects': + case 'goroutine': + case 'samples': + unit = 'objects' + break + case 'cpu': + unit = 'samples' + sampleRate = 1000000000 + } + /** @type {flamebearerV1} */ + const flameBearer = { + levels: fg.getLevelsList().map(l => l.getValuesList().map(v => v)), + maxSelf: fg.getMaxSelf(), + names: fg.getNamesList(), + numTicks: fg.getTotal() + } + /** @type {flamebearerMetadataV1} */ + const metadata = { + format: 'single', + units: unit, + name: profileType.getSampleType(), + sampleRate: sampleRate + } - err = g.Wait() - if err != nil { - httputil.Error(w, err) - return - } + return { + version: 1, + flamebearerProfileV1: { + metadata: metadata, + flamebearer: flameBearer + } + } +} - seriesVal := &typesv1.Series{} - if len(resSeries.Msg.Series) == 1 { - seriesVal = resSeries.Msg.Series[0] - } +/** + * + * @param fromSec {number} + * @param toSec {number} + * @returns {number} + */ +function calcIntervalSec (fromSec, toSec) { + return Math.max(Math.ceil((toSec - fromSec) / 1500), 15) +} - fb := phlaremodel.ExportToFlamebearer(resFlame.Msg.Flamegraph, profileType) - fb.Timeline = timeline.New(seriesVal, selectParams.Start, selectParams.End, int64(timelineStep)) +/** + * + * @param series + * @param startMs + * @param endMs + * @param durationDeltaSec + * @returns {flamebearerTimelineV1} + */ +function timeline (series, startMs, endMs, durationDeltaSec) { + const durationDeltaMs = durationDeltaSec * 1000 + startMs = Math.floor(startMs / durationDeltaMs) * durationDeltaMs + endMs = Math.floor(endMs / durationDeltaMs) * durationDeltaMs + const startS = Math.floor(startMs / 1000) + /** @type {flamebearerTimelineV1} */ + const timeline = { + durationDelta: durationDeltaSec, + startTime: startS, + samples: [] + } + if (startMs >= endMs) { + return timeline + } + const points = boundPointsToWindow(series.getPointsList(), startMs, endMs) + if (points.length < 1) { + const n = sizeToBackfill(startMs, endMs, durationDeltaSec) + if (n > 0) { + timeline.samples = new Array(n).fill(0) + } + return timeline + } - if len(groupBy) > 0 { - fb.Groups = make(map[string]*flamebearer.FlamebearerTimelineV1) - for _, s := range resSeries.Msg.Series { - key := "*" - for _, l := range s.Labels { - // right now we only support one group by - if l.Name == groupBy[0] { - key = l.Value - break - } - } - fb.Groups[key] = timeline.New(s, selectParams.Start, selectParams.End, int64(timelineStep)) - } - } + let n = sizeToBackfill(startMs, parseInt(points[0].getTimestamp()), durationDeltaSec) + const samples = n > 0 ? Array(n).fill(0) : [] + let prev = points[0] + for (const p of points) { + n = sizeToBackfill(parseInt(prev.getTimestamp()), parseInt(p.getTimestamp()), durationDeltaSec) + Array.prototype.push.apply(samples, new Array(Math.max(0, n - 1)).fill(0)) + samples.push(p.getValue()) + prev = p + } + Array.prototype.push.apply(samples, + new Array(Math.max(0, sizeToBackfill(startMs, endMs, durationDeltaSec) - samples.length)) + .fill(0) + ) + timeline.samples = samples + return timeline +} - w.Header().Add("Content-Type", "application/json") - if err := json.NewEncoder(w).Encode(fb); err != nil { - httputil.Error(w, err) - return - } +/** + * + * @param points {[]} + * @param startMs {number} + * @param endMs {number} + */ +function boundPointsToWindow (points, startMs, endMs) { + const startIdx = points.findIndex((v) => v.getTimestamp() >= startMs) + const endIdx = points.findLastIndex((v) => v.getTimestamp() < endMs) + return points.slice(startIdx, endIdx + 1) } + +/** + * + * @param startMs {number} + * @param endMs {number} + * @param stepSec {number} + * @returns {number} */ +function sizeToBackfill (startMs, endMs, stepSec) { + return Math.floor((endMs - startMs) / (stepSec * 1000)) +} /** * @@ -120,11 +227,19 @@ const parseQuery = (query) => { labels.push([m[2], m[3], m[4]]) } } + const profileType = new types.ProfileType() + profileType.setId(typeId) + profileType.setName(typeDesc.type) + profileType.setSampleType(typeDesc.sampleType) + profileType.setSampleUnit(typeDesc.sampleUnit) + profileType.setPeriodType(typeDesc.periodType) + profileType.setPeriodUnit(typeDesc.periodUnit) return { typeId, typeDesc, labels, - labelSelector: strLabels + labelSelector: strLabels, + profileType } } diff --git a/pyroscope/select_series.js b/pyroscope/select_series.js new file mode 100644 index 00000000..0cdeaa0a --- /dev/null +++ b/pyroscope/select_series.js @@ -0,0 +1,142 @@ +const { QrynBadRequest } = require('../lib/handlers/errors') +const { parseTypeId, serviceNameSelectorQuery, labelSelectorQuery } = require('./shared') +const { clusterName } = require('../common') +const Sql = require('@cloki/clickhouse-sql') +const { DATABASE_NAME } = require('../lib/utils') +const types = require('./types/v1/types_pb') +const clickhouse = require('../lib/db/clickhouse') +const messages = require('./querier_pb') + +const selectSeriesImpl = async (fromTimeSec, toTimeSec, payload) => { + const _req = payload + let typeID = _req.getProfileTypeid && _req.getProfileTypeid() + if (!typeID) { + throw new QrynBadRequest('No type provided') + } + typeID = parseTypeId(typeID) + if (!typeID) { + throw new QrynBadRequest('Invalid type provided') + } + const dist = clusterName ? '_dist' : '' + const sampleTypeId = typeID.sampleType + ':' + typeID.sampleUnit + const labelSelector = _req.getLabelSelector && _req.getLabelSelector() + let groupBy = _req.getGroupByList && _req.getGroupByList() + groupBy = groupBy && groupBy.length ? groupBy : null + const step = _req.getStep && parseInt(_req.getStep()) + if (!step || isNaN(step)) { + throw new QrynBadRequest('No step provided') + } + const aggregation = _req.getAggregation && _req.getAggregation() + + const typeIdSelector = Sql.Eq( + 'type_id', + Sql.val(`${typeID.type}:${typeID.periodType}:${typeID.periodUnit}`)) + const serviceNameSelector = serviceNameSelectorQuery(labelSelector) + + const idxReq = (new Sql.Select()) + .select(new Sql.Raw('fingerprint')) + .from(`${DATABASE_NAME()}.profiles_series_gin`) + .where( + Sql.And( + typeIdSelector, + serviceNameSelector, + Sql.Gte('date', new Sql.Raw(`toDate(FROM_UNIXTIME(${Math.floor(fromTimeSec)}))`)), + Sql.Lte('date', new Sql.Raw(`toDate(FROM_UNIXTIME(${Math.floor(toTimeSec)}))`)), + Sql.Eq(new Sql.Raw( + `has(sample_types_units, (${Sql.quoteVal(typeID.sampleType)}, ${Sql.quoteVal(typeID.sampleUnit)}))`), + 1) + ) + ) + labelSelectorQuery(idxReq, labelSelector) + + const withIdxReq = (new Sql.With('idx', idxReq, !!clusterName)) + + let tagsReq = 'arraySort(p.tags)' + if (groupBy) { + tagsReq = `arraySort(arrayFilter(x -> x.1 in (${groupBy.map(g => Sql.quoteVal(g)).join(',')}), p.tags))` + } + + const labelsReq = (new Sql.Select()).with(withIdxReq).select( + 'fingerprint', + [new Sql.Raw(tagsReq), 'tags'], + [groupBy ? new Sql.Raw('cityHash64(tags)') : 'fingerprint', 'new_fingerprint'] + ).distinct(true).from([`${DATABASE_NAME()}.profiles_series`, 'p']) + .where(Sql.And( + new Sql.In('fingerprint', 'IN', new Sql.WithReference(withIdxReq)), + Sql.Gte('date', new Sql.Raw(`toDate(FROM_UNIXTIME(${Math.floor(fromTimeSec)}))`)), + Sql.Lte('date', new Sql.Raw(`toDate(FROM_UNIXTIME(${Math.floor(toTimeSec)}))`)), + typeIdSelector, + serviceNameSelector + )) + + const withLabelsReq = new Sql.With('labels', labelsReq, !!clusterName) + + let valueCol = new Sql.Raw( + `sum(toFloat64(arrayFirst(x -> x.1 == ${Sql.quoteVal(sampleTypeId)}, p.values_agg).2))`) + if (aggregation === types.TimeSeriesAggregationType.TIME_SERIES_AGGREGATION_TYPE_AVERAGE) { + valueCol = new Sql.Raw( + `sum(toFloat64(arrayFirst(x -> x.1 == ${Sql.quoteVal(sampleTypeId)}).2, p.values_agg)) / ` + + `sum(toFloat64(arrayFirst(x -> x.1 == ${Sql.quoteVal(sampleTypeId)}).3, p.values_agg))` + ) + } + + const mainReq = (new Sql.Select()).with(withIdxReq, withLabelsReq).select( + [new Sql.Raw(`intDiv(p.timestamp_ns, 1000000000 * ${step}) * ${step} * 1000`), 'timestamp_ms'], + [new Sql.Raw('labels.new_fingerprint'), 'fingerprint'], + [new Sql.Raw('min(labels.tags)'), 'labels'], + [valueCol, 'value'] + ).from([`${DATABASE_NAME()}.profiles${dist}`, 'p']).join( + [new Sql.WithReference(withLabelsReq), 'labels'], + 'ANY LEFT', + Sql.Eq(new Sql.Raw('p.fingerprint'), new Sql.Raw('labels.fingerprint')) + ).where( + Sql.And( + new Sql.In('p.fingerprint', 'IN', new Sql.WithReference(withIdxReq)), + Sql.Gte('p.timestamp_ns', new Sql.Raw(`${fromTimeSec}000000000`)), + Sql.Lt('p.timestamp_ns', new Sql.Raw(`${toTimeSec}000000000`)), + typeIdSelector, + serviceNameSelector + ) + ).groupBy('timestamp_ns', 'fingerprint') + .orderBy(['fingerprint', 'ASC'], ['timestamp_ns', 'ASC']) + const strMainReq = mainReq.toString() + const chRes = await clickhouse + .rawRequest(strMainReq + ' FORMAT JSON', null, DATABASE_NAME()) + + let lastFingerprint = null + const seriesList = [] + let lastSeries = null + let lastPoints = [] + for (let i = 0; i < chRes.data.data.length; i++) { + const e = chRes.data.data[i] + if (lastFingerprint !== e.fingerprint) { + lastFingerprint = e.fingerprint + lastSeries && lastSeries.setPointsList(lastPoints) + lastSeries && seriesList.push(lastSeries) + lastPoints = [] + lastSeries = new types.Series() + lastSeries.setLabelsList(e.labels.map(l => { + const lp = new types.LabelPair() + lp.setName(l[0]) + lp.setValue(l[1]) + return lp + })) + } + + const p = new types.Point() + p.setValue(e.value) + p.setTimestamp(e.timestamp_ms) + lastPoints.push(p) + } + lastSeries && lastSeries.setPointsList(lastPoints) + lastSeries && seriesList.push(lastSeries) + + const resp = new messages.SelectSeriesResponse() + resp.setSeriesList(seriesList) + console.log(`Queried ${seriesList.length} series`) + return resp +} + +module.exports = { + selectSeriesImpl +} diff --git a/pyroscope/shared.js b/pyroscope/shared.js index 2a37005a..c5f4d136 100644 --- a/pyroscope/shared.js +++ b/pyroscope/shared.js @@ -171,6 +171,8 @@ const labelSelectorQuery = (query, labelSelector) => { )) } +const HISTORY_TIMESPAN = 1000 * 60 * 60 * 24 * 7 + module.exports = { bufferize, parser, @@ -179,5 +181,6 @@ module.exports = { parseTypeId, serviceNameSelectorQuery, parseLabelSelector, - labelSelectorQuery + labelSelectorQuery, + HISTORY_TIMESPAN } From fa3e67d983026371dc2b60c969482720649bf3bc Mon Sep 17 00:00:00 2001 From: akvlad Date: Wed, 21 Aug 2024 17:25:50 +0300 Subject: [PATCH 4/8] render-diff endpoint impl --- patterns/patterns_bin/src/pattern_reg.rs | 45 +++ patterns/patterns_bin/src/tokens.rs | 45 +++ pyroscope/flamebearer.d.ts | 13 + pyroscope/merge_stack_traces.js | 54 ++- pyroscope/pprof-bin/pkg/pprof_bin.d.ts | 7 + pyroscope/pprof-bin/pkg/pprof_bin.js | 22 + pyroscope/pprof-bin/pkg/pprof_bin_bg.wasm | Bin 197071 -> 225243 bytes .../pprof-bin/pkg/pprof_bin_bg.wasm.d.ts | 1 + pyroscope/pprof-bin/src/lib.rs | 378 ++++++++++++++++-- pyroscope/pyroscope.js | 2 + pyroscope/render.js | 4 +- pyroscope/render_diff.js | 75 ++++ 12 files changed, 585 insertions(+), 61 deletions(-) create mode 100644 patterns/patterns_bin/src/pattern_reg.rs create mode 100644 patterns/patterns_bin/src/tokens.rs create mode 100644 pyroscope/render_diff.js diff --git a/patterns/patterns_bin/src/pattern_reg.rs b/patterns/patterns_bin/src/pattern_reg.rs new file mode 100644 index 00000000..f8657961 --- /dev/null +++ b/patterns/patterns_bin/src/pattern_reg.rs @@ -0,0 +1,45 @@ +use crate::pattern::Pattern; +use uuid::Uuid; + +pub struct PatternRegistry { + patterns: Vec, +} + +impl PatternRegistry { + pub const fn new() -> PatternRegistry { + PatternRegistry { patterns: Vec::new() } + } + + pub fn find_pattern(&mut self, str_text: &Vec, i_text: &Vec, sample: String) -> &Pattern { + let mut idx: i32 = -1; + let mut mtc = 0; + for i in 0..self.patterns.len() { + mtc = self.patterns[i].match_text(&i_text); + if mtc == -1 || mtc > self.patterns[i].fluct { + continue; + } + idx = i as i32; + break; + } + + if idx == -1 { + let pattern = Pattern::new(Uuid::new_v4().to_string(), &i_text, &str_text, sample); + self.patterns.push(pattern); + idx = (self.patterns.len() - 1) as i32; + } else if mtc != 0 { + self.patterns[idx as usize].adjust_pattern(&i_text); + } + return &self.patterns[idx as usize]; + } + + pub fn to_string(&self) -> String { + let mut s = String::new(); + for i in 0..self.patterns.len() { + s += self.patterns[i].to_string().as_str(); + s += "\n"; + } + return s + } +} + +pub static mut REGISTRY: PatternRegistry = PatternRegistry::new(); \ No newline at end of file diff --git a/patterns/patterns_bin/src/tokens.rs b/patterns/patterns_bin/src/tokens.rs new file mode 100644 index 00000000..5d3f4449 --- /dev/null +++ b/patterns/patterns_bin/src/tokens.rs @@ -0,0 +1,45 @@ +use regex::{Regex, CaptureMatches, Match}; + +/*pub fn tokenize(re: &Regex, text: &str) -> CaptureMatches { + return re.captures_iter(text); +}*/ + +pub struct Tokenizer<'a> { + text: String, + pos: usize, + re: Regex, + iter: Option> +} + +impl Tokenizer<'_> { + pub fn new<'a>(text: &'a str) -> Tokenizer<'a> { + let mut res = Tokenizer { + text: text.to_string(), + pos: 0, + re: Regex::new(r"([\p{L}_]+|[\d.]+|[^\p{L}_\d.]+)\s*").unwrap(), + iter: None + }; + res + } +} + +impl Iterator for Tokenizer<'_> { + type Item = String; + + fn next(&mut self) -> Option { + None + /*let cap: Option = None; + if let Some(c) = cap { + self.pos += c.get(0).unwrap().end(); + Some(c.get(0).unwrap().as_str().to_string()) + } else { + None + }*/ + } +} + +#[test] +fn test_tokenizer() { + let text = "Hello, world! 123"; + let mut tokenizer = Tokenizer::new(text); +} \ No newline at end of file diff --git a/pyroscope/flamebearer.d.ts b/pyroscope/flamebearer.d.ts index 27a701dd..7ec2f880 100644 --- a/pyroscope/flamebearer.d.ts +++ b/pyroscope/flamebearer.d.ts @@ -52,3 +52,16 @@ export interface heatmap { minDepth: uint64, maxDepth: uint64 } + +export interface level { + values: number[] +} + +export interface flamegraphDiff { + name: string[], + levels: level[], + total: int64, + maxSelf: int64, + leftTicks: int64, + rightTicks: int64 +} diff --git a/pyroscope/merge_stack_traces.js b/pyroscope/merge_stack_traces.js index c9033f4e..f8664712 100644 --- a/pyroscope/merge_stack_traces.js +++ b/pyroscope/merge_stack_traces.js @@ -22,7 +22,9 @@ const sqlWithReference = (ref) => { let ctxIdx = 0 -const mergeStackTraces = async (typeRegex, sel, fromTimeSec, toTimeSec, log) => { +const newCtxIdx = () => ++ctxIdx + +const importStackTraces = async (typeRegex, sel, fromTimeSec, toTimeSec, log, _ctxIdx, save) => { const dist = clusterName ? '_dist' : '' const v2 = checkVersion('profiles_v2', (fromTimeSec - 3600) * 1000) const serviceNameSelector = serviceNameSelectorQuery(sel) @@ -127,30 +129,36 @@ const mergeStackTraces = async (typeRegex, sel, fromTimeSec, toTimeSec, log) => const binData = Uint8Array.from(profiles.data) log.debug(`selectMergeStacktraces: profiles downloaded: ${binData.length / 1025}kB in ${Date.now() - start}ms`) require('./pprof-bin/pkg/pprof_bin').init_panic_hook() - const _ctxIdx = ++ctxIdx const [legacyLen, shift] = readULeb32(binData, 0) let ofs = shift - try { - let mergePprofLat = BigInt(0) - for (let i = 0; i < legacyLen; i++) { - const [profLen, shift] = readULeb32(binData, ofs) - ofs += shift - start = process.hrtime?.bigint ? process.hrtime.bigint() : BigInt(0) - pprofBin.merge_prof(_ctxIdx, - Uint8Array.from(profiles.data.slice(ofs, ofs + profLen)), - `${typeRegex.sampleType}:${typeRegex.sampleUnit}`) - mergePprofLat += (process.hrtime?.bigint ? process.hrtime.bigint() : BigInt(0)) - start - ofs += profLen - } - start = process.hrtime?.bigint ? process.hrtime.bigint() : BigInt(0) - pprofBin.merge_tree(_ctxIdx, Uint8Array.from(profiles.data.slice(ofs)), - typeRegex.sampleType + ':' + typeRegex.sampleUnit) - const mergeTreeLat = (process.hrtime?.bigint ? process.hrtime.bigint() : BigInt(0)) - start + let mergePprofLat = BigInt(0) + for (let i = 0; i < legacyLen; i++) { + const [profLen, shift] = readULeb32(binData, ofs) + ofs += shift start = process.hrtime?.bigint ? process.hrtime.bigint() : BigInt(0) + pprofBin.merge_prof(_ctxIdx, + Uint8Array.from(profiles.data.slice(ofs, ofs + profLen)), + `${typeRegex.sampleType}:${typeRegex.sampleUnit}`) + mergePprofLat += (process.hrtime?.bigint ? process.hrtime.bigint() : BigInt(0)) - start + ofs += profLen + } + start = process.hrtime?.bigint ? process.hrtime.bigint() : BigInt(0) + save && require('fs').writeFileSync(`/home/hromozeka/QXIP/qryn/data.${Date.now()}.bin`, + Buffer.from(Uint8Array.from(profiles.data.slice(ofs)))) + pprofBin.merge_tree(_ctxIdx, Uint8Array.from(profiles.data.slice(ofs)), + typeRegex.sampleType + ':' + typeRegex.sampleUnit) + const mergeTreeLat = (process.hrtime?.bigint ? process.hrtime.bigint() : BigInt(0)) - start + log.debug(`merge_pprof: ${mergePprofLat / BigInt(1000000)}ms`) + log.debug(`merge_tree: ${mergeTreeLat / BigInt(1000000)}ms`) +} + +const mergeStackTraces = async (typeRegex, sel, fromTimeSec, toTimeSec, log) => { + const _ctxIdx = newCtxIdx() + try { + await importStackTraces(typeRegex, sel, fromTimeSec, toTimeSec, log, _ctxIdx) + const start = process.hrtime?.bigint ? process.hrtime.bigint() : BigInt(0) const resp = pprofBin.export_tree(_ctxIdx, typeRegex.sampleType + ':' + typeRegex.sampleUnit) const exportTreeLat = (process.hrtime?.bigint ? process.hrtime.bigint() : BigInt(0)) - start - log.debug(`merge_pprof: ${mergePprofLat / BigInt(1000000)}ms`) - log.debug(`merge_tree: ${mergeTreeLat / BigInt(1000000)}ms`) log.debug(`export_tree: ${exportTreeLat / BigInt(1000000)}ms`) return Buffer.from(resp) } finally { @@ -159,5 +167,7 @@ const mergeStackTraces = async (typeRegex, sel, fromTimeSec, toTimeSec, log) => } module.exports = { - mergeStackTraces -} \ No newline at end of file + mergeStackTraces, + importStackTraces, + newCtxIdx +} diff --git a/pyroscope/pprof-bin/pkg/pprof_bin.d.ts b/pyroscope/pprof-bin/pkg/pprof_bin.d.ts index f4204d23..ccbddd41 100644 --- a/pyroscope/pprof-bin/pkg/pprof_bin.d.ts +++ b/pyroscope/pprof-bin/pkg/pprof_bin.d.ts @@ -13,6 +13,13 @@ export function merge_prof(id: number, bytes: Uint8Array, sample_type: string): */ export function merge_tree(id: number, bytes: Uint8Array, sample_type: string): void; /** +* @param {number} id1 +* @param {number} id2 +* @param {string} sample_type +* @returns {Uint8Array} +*/ +export function diff_tree(id1: number, id2: number, sample_type: string): Uint8Array; +/** * @param {number} id * @param {string} sample_type * @returns {Uint8Array} diff --git a/pyroscope/pprof-bin/pkg/pprof_bin.js b/pyroscope/pprof-bin/pkg/pprof_bin.js index 913b1d40..25da605f 100644 --- a/pyroscope/pprof-bin/pkg/pprof_bin.js +++ b/pyroscope/pprof-bin/pkg/pprof_bin.js @@ -133,6 +133,28 @@ function getArrayU8FromWasm0(ptr, len) { ptr = ptr >>> 0; return getUint8Memory0().subarray(ptr / 1, ptr / 1 + len); } +/** +* @param {number} id1 +* @param {number} id2 +* @param {string} sample_type +* @returns {Uint8Array} +*/ +module.exports.diff_tree = function(id1, id2, sample_type) { + try { + const retptr = wasm.__wbindgen_add_to_stack_pointer(-16); + const ptr0 = passStringToWasm0(sample_type, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); + const len0 = WASM_VECTOR_LEN; + wasm.diff_tree(retptr, id1, id2, ptr0, len0); + var r0 = getInt32Memory0()[retptr / 4 + 0]; + var r1 = getInt32Memory0()[retptr / 4 + 1]; + var v2 = getArrayU8FromWasm0(r0, r1).slice(); + wasm.__wbindgen_free(r0, r1 * 1, 1); + return v2; + } finally { + wasm.__wbindgen_add_to_stack_pointer(16); + } +}; + /** * @param {number} id * @param {string} sample_type diff --git a/pyroscope/pprof-bin/pkg/pprof_bin_bg.wasm b/pyroscope/pprof-bin/pkg/pprof_bin_bg.wasm index fcb06ee59eb2fcdb74c0efa14abc99952546d069..a110fa20b1bcf70162a8c29840339d66ce3b1f53 100644 GIT binary patch literal 225243 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 literal 197071 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ྯ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 z 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 L-F*06LWBMf4jYP6 diff --git a/pyroscope/pprof-bin/pkg/pprof_bin_bg.wasm.d.ts b/pyroscope/pprof-bin/pkg/pprof_bin_bg.wasm.d.ts index c1259bcb..6dc10bc2 100644 --- a/pyroscope/pprof-bin/pkg/pprof_bin_bg.wasm.d.ts +++ b/pyroscope/pprof-bin/pkg/pprof_bin_bg.wasm.d.ts @@ -3,6 +3,7 @@ export const memory: WebAssembly.Memory; export function merge_prof(a: number, b: number, c: number, d: number, e: number): void; export function merge_tree(a: number, b: number, c: number, d: number, e: number): void; +export function diff_tree(a: number, b: number, c: number, d: number, e: number): void; export function export_tree(a: number, b: number, c: number, d: number): void; export function export_trees_pprof(a: number, b: number, c: number): void; export function drop_tree(a: number): void; diff --git a/pyroscope/pprof-bin/src/lib.rs b/pyroscope/pprof-bin/src/lib.rs index 493d3992..e3985831 100644 --- a/pyroscope/pprof-bin/src/lib.rs +++ b/pyroscope/pprof-bin/src/lib.rs @@ -2,6 +2,7 @@ mod ch64; mod merge; +use std::cmp::Ordering; use ch64::city_hash_64; use ch64::read_uint64_le; use lazy_static::lazy_static; @@ -12,6 +13,7 @@ use pprof_pb::google::v1::Sample; use pprof_pb::querier::v1::FlameGraph; use pprof_pb::querier::v1::Level; use pprof_pb::querier::v1::SelectMergeStacktracesResponse; +use pprof_pb::querier::v1::FlameGraphDiff; use prost::Message; use std::collections::{HashMap, HashSet}; use std::io::Read; @@ -19,6 +21,8 @@ use std::panic; use std::sync::Mutex; use std::vec::Vec; use wasm_bindgen::prelude::*; +use std::sync::Arc; + pub mod pprof_pb { @@ -47,16 +51,51 @@ struct TreeNodeV2 { total: Vec, } +impl TreeNodeV2 { + pub fn clone(&self) -> TreeNodeV2 { + TreeNodeV2 { + fn_id: self.fn_id, + node_id: self.node_id, + slf: self.slf.clone(), + total: self.total.clone(), + } + } + pub fn set_total_and_self(&self, slf: Vec, total: Vec) -> TreeNodeV2 { + let mut res = self.clone(); + res.slf = slf; + res.total = total; + return res; + } +} + struct Tree { names: Vec, names_map: HashMap, - nodes: HashMap>, + nodes: HashMap>>, sample_types: Vec, max_self: Vec, nodes_num: i32, } -fn find_node(id: u64, nodes: &Vec) -> i32 { +impl Tree { + pub fn total(&self) -> i64 { + let mut total: i64 = 0; + for c in 0..self.nodes.get(&0).unwrap().len() { + let _c = &self.nodes.get(&0).unwrap()[c]; + total += _c.total[0]; + } + total + } + pub fn add_name(&mut self, name: String, name_hash: u64) { + if self.names_map.contains_key(&name_hash) { + return; + } + self.names.push(name); + self.names_map.insert(name_hash, self.names.len() - 1); + } +} + +fn find_node(id: u64, nodes: &Vec>) -> i32 { let mut n: i32 = -1; for c in 0..nodes.len() { let _c = &nodes[c]; @@ -109,19 +148,25 @@ impl MergeTotalsProcessor { fn merge_totals( &self, - node: &mut TreeNodeV2, + node: Arc, _max_self: &Vec, sample: &Sample, merge_self: bool, - ) -> Vec { + ) -> (TreeNodeV2, Vec) { let mut max_self = _max_self.clone(); + let mut res: TreeNodeV2 = TreeNodeV2 { + fn_id: node.fn_id, + node_id: node.node_id, + slf: vec![0; node.slf.len()], + total: vec![0; node.slf.len()], + }; for i in 0..self.from_idx.len() { if self.from_idx[i] == -1 { continue; } - node.total[i] += sample.value[self.from_idx[i] as usize]; + res.total[i] += sample.value[self.from_idx[i] as usize]; if merge_self { - node.slf[i] += sample.value[self.from_idx[i] as usize]; + res.slf[i] += sample.value[self.from_idx[i] as usize]; for i in 0..max_self.len() { if max_self[i] < node.slf[i] { max_self[i] = node.slf[i]; @@ -129,7 +174,7 @@ impl MergeTotalsProcessor { } } } - max_self + (res, max_self) } } @@ -164,29 +209,30 @@ fn merge(tree: &mut Tree, p: &Profile) { if !tree.nodes.contains_key(&parent_id) && tree.nodes_num < 2000000 { tree.nodes.insert(parent_id, Vec::new()); } - let mut fake_children: Vec = Vec::new(); + let mut fake_children: Vec> = Vec::new(); let children = tree.nodes.get_mut(&parent_id).unwrap_or(&mut fake_children); let mut n = find_node(node_id, children); if n == -1 { - children.push(TreeNodeV2 { + children.push(Arc::new(TreeNodeV2 { //parent_id, fn_id: name_hash, node_id, slf: vec![0; tree.sample_types.len()], total: vec![0; tree.sample_types.len()], - }); + })); let idx = children.len().clone() - 1; - let max_self = m.merge_totals( - children.get_mut(idx).unwrap(), + let new_node_and_max_self = m.merge_totals( + children.get(idx).unwrap().clone(), tree.max_self.as_ref(), s, i == 0, ); - tree.max_self = max_self; + children[idx] = Arc::new(new_node_and_max_self.0); + tree.max_self = new_node_and_max_self.1; n = idx as i32; } else if tree.nodes_num < 2000000 { m.merge_totals( - children.get_mut(n as usize).unwrap(), + children.get_mut(n as usize).unwrap().clone(), &tree.max_self, s, i == 0, @@ -214,7 +260,7 @@ fn read_uleb128(bytes: &[u8]) -> (usize, usize) { fn bfs(t: &Tree, res: &mut Vec, sample_type: String) { let mut total: i64 = 0; - let mut root_children: &Vec = &Vec::new(); + let mut root_children: &Vec> = &Vec::new(); if t.nodes.contains_key(&(0u64)) { root_children = t.nodes.get(&(0u64)).unwrap(); } @@ -291,22 +337,22 @@ fn bfs(t: &Tree, res: &mut Vec, sample_type: String) { } lazy_static! { - static ref CTX: Mutex> = Mutex::new(HashMap::new()); + static ref CTX: Mutex>> = Mutex::new(HashMap::new()); } -fn upsert_tree(ctx: &mut HashMap, id: u32, sample_types: Vec) { +fn upsert_tree(ctx: &mut HashMap>, id: u32, sample_types: Vec) { if !ctx.contains_key(&id) { let _len = sample_types.len().clone(); ctx.insert( id, - Tree { + Mutex::new(Tree { names: vec!["total".to_string(), "n/a".to_string()], names_map: HashMap::new(), nodes: HashMap::new(), sample_types, max_self: vec![0; _len], nodes_num: 1, - }, + }), ); } } @@ -413,18 +459,11 @@ fn merge_trie(tree: &mut Tree, bytes: &[u8], samples_type: &String) { n = find_node(node_id, tree.nodes.get(&parent_id).unwrap()); } if n != -1 { - tree.nodes - .get_mut(&parent_id) - .unwrap() - .get_mut(n as usize) - .unwrap() - .total[sample_type_index] += total[sample_type_index]; - tree.nodes - .get_mut(&parent_id) - .unwrap() - .get_mut(n as usize) - .unwrap() - .slf[sample_type_index] += slf[sample_type_index]; + let mut __node = tree.nodes.get_mut(&parent_id).unwrap().get_mut(n as usize).unwrap().clone(); + let mut _node = __node.as_ref().clone(); + _node.total[sample_type_index] += total[sample_type_index]; + _node.slf[sample_type_index] += slf[sample_type_index]; + tree.nodes.get_mut(&parent_id).unwrap()[n as usize] = Arc::new(_node); } if tree.nodes_num >= 2000000 { return; @@ -432,13 +471,13 @@ fn merge_trie(tree: &mut Tree, bytes: &[u8], samples_type: &String) { if !tree.nodes.contains_key(&parent_id) { tree.nodes.insert(parent_id, Vec::new()); } - tree.nodes.get_mut(&parent_id).unwrap().push(TreeNodeV2 { + tree.nodes.get_mut(&parent_id).unwrap().push(Arc::new(TreeNodeV2 { fn_id, //parent_id, node_id, slf, total, - }); + })); tree.nodes_num += 1; } } @@ -551,12 +590,25 @@ fn merge_trie(tree: &mut Tree, bytes: &[u8], samples_type: &String) { inject_functions(prof, tree, 0, vec![], type_idx); }*/ +fn assert_positive(t: &Tree) -> bool{ + for n in t.nodes.keys() { + for _n in 0..t.nodes.get(&n).unwrap().len() { + for __n in 0..t.nodes.get(&n).unwrap()[_n].slf.len() { + if t.nodes.get(&n).unwrap()[_n].slf[__n] < 0 { + return false; + } + } + } + } + true +} + #[wasm_bindgen] pub fn merge_prof(id: u32, bytes: &[u8], sample_type: String) { let p = panic::catch_unwind(|| { let mut ctx = CTX.lock().unwrap(); upsert_tree(&mut ctx, id, vec![sample_type]); - let mut tree = ctx.get_mut(&id).unwrap(); + let mut tree = ctx.get_mut(&id).unwrap().lock().unwrap(); let prof = Profile::decode(bytes).unwrap(); merge(&mut tree, &prof); }); @@ -571,7 +623,7 @@ pub fn merge_tree(id: u32, bytes: &[u8], sample_type: String) { let result = panic::catch_unwind(|| { let mut ctx = CTX.lock().unwrap(); upsert_tree(&mut ctx, id, vec![sample_type.clone()]); - let mut tree = ctx.get_mut(&id).unwrap(); + let mut tree = ctx.get_mut(&id).unwrap().lock().unwrap(); merge_trie(&mut tree, bytes, &sample_type); 0 }); @@ -581,25 +633,275 @@ pub fn merge_tree(id: u32, bytes: &[u8], sample_type: String) { } } +#[wasm_bindgen] +pub fn diff_tree(id1: u32, id2: u32, sample_type: String) -> Vec { + let mut ctx = CTX.lock().unwrap(); + let _ctx = &mut ctx; + upsert_tree(_ctx, id1, vec![sample_type.clone()]); + upsert_tree(_ctx, id2, vec![sample_type.clone()]); + let mut t1 = _ctx.get(&id1).unwrap().lock().unwrap(); + let mut t2 = _ctx.get(&id2).unwrap().lock().unwrap(); + let mut is_positive = assert_positive(&t1); + if !is_positive { + panic!("Tree 1 is not positive"); + } + is_positive = assert_positive(&t2); + if!is_positive { + panic!("Tree 2 is not positive"); + } + + + for n in t1.names_map.keys() { + if !t2.names_map.contains_key(&n) { + t2.names.push(t1.names[*t1.names_map.get(&n).unwrap()].clone()); + let idx = t2.names.len() - 1; + t2.names_map.insert(*n, idx); + } + } + for n in t2.names_map.keys() { + if !t1.names_map.contains_key(&n) { + let idx = t2.names_map.get(&n).unwrap().clone(); + t1.names.push(t2.names[idx].clone()); + let idx2 = t1.names.len() - 1; + t1.names_map.insert(*n, idx2); + } + } + + let keys = t1.nodes.keys().map(|x| (*x).clone()).collect::>(); + for n in keys { + if !t2.nodes.contains_key(&n) { + t2.nodes.insert(n, vec![]); + } + let lnodes = t1.nodes.get_mut(&n).unwrap(); + let rnodes = t2.nodes.get_mut(&n).unwrap(); + lnodes.sort_by(|x, y| + if x.node_id < y.node_id { Ordering::Less } else { Ordering::Greater }); + rnodes.sort_by(|x, y| + if x.node_id < y.node_id { Ordering::Less } else { Ordering::Greater }); + let mut i = 0; + let mut j = 0; + let mut new_t1_nodes: Vec> = vec![]; + let mut new_t2_nodes: Vec> = vec![]; + let t1_nodes = t1.nodes.get(&n).unwrap(); + let t2_nodes = t2.nodes.get(&n).unwrap(); + while i < t1_nodes.len() && j < t2_nodes.len() { + if n == 0 { + println!("{:?}:{:?} - {:?}:{:?}", + t1_nodes[i].node_id, + t1.names[*t1.names_map.get(&t1_nodes[i].fn_id).unwrap() as usize], + t2_nodes[j].node_id, + t2.names[*t2.names_map.get(&t2_nodes[j].fn_id).unwrap() as usize] + ) + } + + if t1_nodes[i].node_id == t2_nodes[j].node_id { + new_t1_nodes.push(t1_nodes[i].clone()); + new_t2_nodes.push(t2_nodes[j].clone()); + i += 1; + j += 1; + continue; + } + if t1_nodes[i].node_id < t2_nodes[j].node_id { + new_t1_nodes.push(t1_nodes[i].clone()); + new_t2_nodes.push(Arc::new(TreeNodeV2{ + node_id: t1_nodes[i].node_id, + fn_id: t1_nodes[i].fn_id, + slf: vec![0], + total: vec![0], + })); + i += 1; + } else { + new_t2_nodes.push(t2_nodes[j].clone()); + new_t1_nodes.push(Arc::new(TreeNodeV2{ + node_id: t2_nodes[j].node_id, + fn_id: t2_nodes[j].fn_id, + slf: vec![0], + total: vec![0], + })); + j += 1; + } + } + while i < t1_nodes.len() { + new_t1_nodes.push(t1_nodes[i].clone()); + new_t2_nodes.push(Arc::new(TreeNodeV2{ + node_id: t1_nodes[i].node_id, + fn_id: t1_nodes[i].fn_id, + slf: vec![0], + total: vec![0], + })); + i += 1; + } + while j < t2_nodes.len() { + new_t2_nodes.push(t2_nodes[j].clone()); + new_t1_nodes.push(Arc::new(TreeNodeV2{ + node_id: t2_nodes[j].node_id, + fn_id: t2_nodes[j].fn_id, + slf: vec![0], + total: vec![0], + })); + j+=1; + } + t1.nodes.insert(n, new_t1_nodes); + t2.nodes.insert(n, new_t2_nodes); + } + + for n in t2.nodes.keys().clone() { + if!t1.nodes.contains_key(&n) { + let mut new_t1_nodes: Vec> = vec![]; + for _n in t2.nodes.get(&n).unwrap() { + new_t1_nodes.push(Arc::new(TreeNodeV2{ + node_id: _n.node_id, + fn_id: _n.fn_id, + slf: vec![0], + total: vec![0], + })) + } + t1.nodes.insert(*n, new_t1_nodes); + } + } + + let total_left = t1.total(); + let total_right = t2.total(); + let mut min_val = 0 as i64; + let tn = Arc::new(TreeNodeV2{ + fn_id: 0, + node_id: 0, + slf: vec![0], + total: vec![total_left], + }); + let mut left_nodes = vec![tn]; + let tn2 = Arc::new(TreeNodeV2{ + fn_id: 0, + node_id: 0, + slf: vec![0], + total: vec![total_right], + }); + let mut right_nodes = vec![tn2]; + + let mut x_left_offsets = vec![0 as i64]; + let mut x_right_offsets = vec![0 as i64]; + let mut levels = vec![0 as i64]; + let mut name_location_cache: HashMap = HashMap::new(); + let mut res = FlameGraphDiff::default(); + res.left_ticks = total_left; + res.right_ticks = total_right; + res.total = total_left + total_right; + while left_nodes.len() > 0 { + let left = left_nodes.pop().unwrap(); + let right = right_nodes.pop().unwrap(); + let mut x_left_offset = x_left_offsets.pop().unwrap(); + let mut x_right_offset = x_right_offsets.pop().unwrap(); + let level = levels.pop().unwrap(); + let mut name: String = "total".to_string(); + if left.fn_id != 0 { + name = t1.names[t1.names_map.get(&left.fn_id).unwrap().clone() as usize].clone(); + } + if left.total[0] >= min_val || right.total[0] >= min_val || name == "other" { + let mut i = 0 as i64; + if !name_location_cache.contains_key(&name) { + res.names.push(name.clone().to_string()); + name_location_cache.insert(name, (res.names.len() - 1) as i64); + i = res.names.len() as i64 - 1; + } else { + i = *name_location_cache.get(name.as_str()).unwrap(); + } + if level == res.levels.len() as i64 { + res.levels.push(Level::default()) + } + if res.max_self < left.slf[0] { + res.max_self = left.slf[0]; + } + if res.max_self < right.slf[0] { + res.max_self = right.slf[0]; + } + let mut values = vec![x_left_offset, left.total[0], left.slf[0], + x_right_offset, right.total[0], right.slf[0], i]; + res.levels[level as usize].values.extend(values); + let mut other_left_total = 0 as i64; + let mut other_right_total = 0 as i64; + let mut nodes_len = 0; + if t1.nodes.contains_key(&left.node_id) { + nodes_len = t1.nodes.get(&left.node_id).unwrap().len().clone(); + } + for j in 0..nodes_len { + let _left = t1.nodes.get(&left.node_id).unwrap()[j].clone(); + let _right = t2.nodes.get(&left.node_id).unwrap()[j].clone(); + if _left.total[0] >= min_val || _right.total[0] >= min_val { + levels.insert(0, level + 1); + x_left_offsets.insert(0, x_left_offset); + x_right_offsets.insert(0, x_right_offset); + x_left_offset += _left.total[0].clone() as i64; + x_right_offset += _right.total[0].clone() as i64; + left_nodes.insert(0, _left.clone()); + right_nodes.insert(0, _right.clone()); + } else { + other_left_total += _left.total[0] as i64; + other_right_total += _right.total[0] as i64; + } + if other_left_total > 0 || other_right_total > 0 { + levels.insert(0, level + 1); + t1.add_name("other".to_string(), 1); + x_left_offsets.insert(0, x_left_offset); + left_nodes.insert(0, Arc::new(TreeNodeV2{ + fn_id: 1, + node_id: 1, + slf: vec![other_left_total as i64], + total: vec![other_left_total as i64], + })); + t2.add_name("other".to_string(), 1); + x_right_offsets.insert(0, x_right_offset); + right_nodes.insert(0, Arc::new(TreeNodeV2{ + fn_id: 1, + node_id: 1, + slf: vec![other_right_total as i64], + total: vec![other_right_total as i64], + })); + } + } + } + + } + for i in 0..res.levels.len() { + let mut j = 0; + let mut prev = 0 as i64; + while j < res.levels[i].values.len() { + res.levels[i].values[j] -= prev; + prev += res.levels[i].values[j] + res.levels[i].values[j+1]; + j += 7; + } + prev = 0; + j = 3; + while j < res.levels[i].values.len() { + res.levels[i].values[j] -= prev; + prev += res.levels[i].values[j] + res.levels[i].values[j+1]; + j += 7; + } + } + + res.encode_to_vec() +} + + + #[wasm_bindgen] pub fn export_tree(id: u32, sample_type: String) -> Vec { let p = panic::catch_unwind(|| { let mut ctx = CTX.lock().unwrap(); let mut res = SelectMergeStacktracesResponse::default(); upsert_tree(&mut ctx, id, vec![sample_type.clone()]); - let tree = ctx.get_mut(&id).unwrap(); + let tree = ctx.get_mut(&id).unwrap().lock().unwrap(); let mut fg = FlameGraph::default(); fg.names = tree.names.clone(); fg.max_self = tree.max_self[0 /* TODO */]; fg.total = 0; - let mut root_children: &Vec = &vec![]; + let mut root_children: &Vec> = &vec![]; if tree.nodes.contains_key(&(0u64)) { root_children = tree.nodes.get(&(0u64)).unwrap(); } for n in root_children.iter() { fg.total += n.total[0 /*TODO*/] as i64; } - bfs(tree, &mut fg.levels, sample_type.clone()); + bfs(&tree, &mut fg.levels, sample_type.clone()); res.flamegraph = Some(fg); return res.encode_to_vec(); }); diff --git a/pyroscope/pyroscope.js b/pyroscope/pyroscope.js index 61ea2a48..f979e10a 100644 --- a/pyroscope/pyroscope.js +++ b/pyroscope/pyroscope.js @@ -9,6 +9,7 @@ const { QrynBadRequest } = require('../lib/handlers/errors') const { clusterName } = require('../common') const logger = require('../lib/logger') const jsonParsers = require('./json_parsers') +const renderDiff = require('./render_diff') const { parser, wrapResponse, @@ -444,4 +445,5 @@ module.exports.init = (fastify) => { } settings.init(fastify) render.init(fastify) + renderDiff.init(fastify) } diff --git a/pyroscope/render.js b/pyroscope/render.js index ce5362c7..c37de551 100644 --- a/pyroscope/render.js +++ b/pyroscope/render.js @@ -248,5 +248,7 @@ const init = (fastify) => { } module.exports = { - init + init, + parseQuery, + toFlamebearer } diff --git a/pyroscope/render_diff.js b/pyroscope/render_diff.js new file mode 100644 index 00000000..c3c46c06 --- /dev/null +++ b/pyroscope/render_diff.js @@ -0,0 +1,75 @@ +const { parseQuery, toFlamebearer } = require('./render') +const { importStackTraces, newCtxIdx } = require('./merge_stack_traces') +const pprofBin = require('./pprof-bin/pkg') +const querierMessages = require('./querier_pb') +const types = require('./types/v1/types_pb') + +const renderDiff = async (req, res) => { + const [leftQuery, leftFromTimeSec, leftToTimeSec] = + parseParams(req.query.leftQuery, req.query.leftFrom, req.query.leftUntil); + const [rightQuery, rightFromTimeSec, rightToTimeSec] = + parseParams(req.query.rightQuery, req.query.rightFrom, req.query.rightUntil); + if (leftQuery.typeId != rightQuery.typeId) { + res.code(400).send('Different type IDs') + return + } + const leftCtxIdx = newCtxIdx() + await importStackTraces(leftQuery.typeDesc, '{' + leftQuery.labelSelector + '}', leftFromTimeSec, leftToTimeSec, req.log, leftCtxIdx, true) + const rightCtxIdx = newCtxIdx() + await importStackTraces(rightQuery.typeDesc, '{' + rightQuery.labelSelector + '}', rightFromTimeSec, rightToTimeSec, req.log, rightCtxIdx, true) + const flamegraphDiffBin = pprofBin.diff_tree(leftCtxIdx, rightCtxIdx, + `${leftQuery.typeDesc.sampleType}:${leftQuery.typeDesc.sampleUnit}`) + const profileType = new types.ProfileType() + profileType.setId(leftQuery.typeId) + profileType.setName(leftQuery.typeDesc.type) + profileType.setSampleType(leftQuery.typeDesc.sampleType) + profileType.setSampleUnit(leftQuery.typeDesc.sampleUnit) + profileType.setPeriodType(leftQuery.typeDesc.periodType) + profileType.setPeriodUnit(leftQuery.typeDesc.periodUnit) + const diff = querierMessages.FlameGraphDiff.deserializeBinary(flamegraphDiffBin) + return res.code(200).send(diffToFlamegraph(diff, profileType).flamebearerProfileV1) +} + +/** + * + * @param diff + * @param type + */ +const diffToFlamegraph = (diff, type) => { + const fg = new querierMessages.FlameGraph() + fg.setNamesList(diff.getNamesList()) + fg.setLevelsList(diff.getLevelsList()) + fg.setTotal(diff.getTotal()) + fg.setMaxSelf(diff.getMaxSelf()) + const fb = toFlamebearer(fg, type) + fb.flamebearerProfileV1.leftTicks = diff.getLeftticks() + fb.flamebearerProfileV1.rightTicks = diff.getRightticks() + fb.flamebearerProfileV1.metadata = { + ...(fb.flamebearerProfileV1.metadata || {}), + format: 'double' + } + return fb +} + +const parseParams = (query, from, until) => { + const parsedQuery = parseQuery(query) + const fromTimeSec = from + ? Math.floor(parseInt(from) / 1000) + : Math.floor((Date.now() - 1000 * 60 * 60 * 48) / 1000) + const toTimeSec = until + ? Math.floor(parseInt(until) / 1000) + : Math.floor((Date.now() - 1000 * 60 * 60 * 48) / 1000) + if (!parsedQuery) { + throw new Error('Invalid query') + } + return [parsedQuery, fromTimeSec, toTimeSec] +} + +const init = (fastify) => { + fastify.get('/pyroscope/render-diff', renderDiff) +} + +module.exports = { + renderDiff, + init +} From 0429e80ecc6f6d6aab98bc401862c951ccf73c52 Mon Sep 17 00:00:00 2001 From: akvlad Date: Wed, 21 Aug 2024 17:36:45 +0300 Subject: [PATCH 5/8] fix clickhouse-cluster E2E --- .github/workflows/node-clickhouse-cluster.js.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/workflows/node-clickhouse-cluster.js.yml b/.github/workflows/node-clickhouse-cluster.js.yml index fbfddb72..50a65a72 100644 --- a/.github/workflows/node-clickhouse-cluster.js.yml +++ b/.github/workflows/node-clickhouse-cluster.js.yml @@ -39,6 +39,11 @@ jobs: - run: npm run postinstall - run: git submodule init - run: git submodule update + - name: Install Compose + uses: ndeloof/install-compose-action@v0.0.1 + with: + version: v2.1.0 # defaults to 'latest' + legacy: true # will also install in PATH as `docker-compose` - run: docker-compose -f docker/e2e/docker-compose-cluster.yaml up -d - run: sleep 5 - name: Workflow Telemetry From 4720bac5ecb308ba25e39feb486fe896f9a708e0 Mon Sep 17 00:00:00 2001 From: akvlad Date: Wed, 21 Aug 2024 17:42:00 +0300 Subject: [PATCH 6/8] silly push --- pyroscope/shared.js | 1 + 1 file changed, 1 insertion(+) diff --git a/pyroscope/shared.js b/pyroscope/shared.js index c5f4d136..fcb45966 100644 --- a/pyroscope/shared.js +++ b/pyroscope/shared.js @@ -102,6 +102,7 @@ const serviceNameSelectorQuery = (labelSelector) => { return conds || empty } + const parseLabelSelector = (labelSelector) => { if (labelSelector.endsWith(',}')) { labelSelector = labelSelector.slice(0, -2) + '}' From 433173f5f8688b52ae2a909b579efdaaa9fb107f Mon Sep 17 00:00:00 2001 From: akvlad Date: Wed, 21 Aug 2024 17:53:59 +0300 Subject: [PATCH 7/8] Fix polynomial regexp --- pyroscope/render.js | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/pyroscope/render.js b/pyroscope/render.js index c37de551..e59837b7 100644 --- a/pyroscope/render.js +++ b/pyroscope/render.js @@ -214,18 +214,21 @@ function sizeToBackfill (startMs, endMs, stepSec) { */ const parseQuery = (query) => { query = query.trim() - const match = query.match(/^([^{]+)\s*(\{(.*)})?$/) + const match = query.match(/^([^{\s]+)\s*(\{(.*)})?$/) if (!match) { return null } const typeId = match[1] const typeDesc = parseTypeId(typeId) - const strLabels = match[3] || '' + let strLabels = (match[3] || '').trim() const labels = [] - if (strLabels && strLabels !== '') { - for (const m in strLabels.matchAll(/([,{])\s*([^A-Za-z0-9_]+)\s*(!=|!~|=~|=)\s*("([^"\\]|\\.)*")/g)) { - labels.push([m[2], m[3], m[4]]) + while (strLabels && strLabels !== '' && strLabels !== '}') { + const m = strLabels.match(/^([,{])\s*([A-Za-z0-9_]+)\s*(!=|!~|=~|=)\s*("([^"\\]|\\.)*")/) + if (!m) { + throw new Error('Invalid label selector') } + labels.push([m[2], m[3], m[4]]) + strLabels = strLabels.substring(m[0].length).trim() } const profileType = new types.ProfileType() profileType.setId(typeId) From 57351e9fc922c68ff15677a168f45f0c5f988b05 Mon Sep 17 00:00:00 2001 From: akvlad Date: Wed, 21 Aug 2024 18:43:56 +0300 Subject: [PATCH 8/8] fix timestamp bug --- pyroscope/render.js | 2 +- pyroscope/select_series.js | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pyroscope/render.js b/pyroscope/render.js index e59837b7..b6fc29ca 100644 --- a/pyroscope/render.js +++ b/pyroscope/render.js @@ -223,7 +223,7 @@ const parseQuery = (query) => { let strLabels = (match[3] || '').trim() const labels = [] while (strLabels && strLabels !== '' && strLabels !== '}') { - const m = strLabels.match(/^([,{])\s*([A-Za-z0-9_]+)\s*(!=|!~|=~|=)\s*("([^"\\]|\\.)*")/) + const m = strLabels.match(/^(,)?\s*([A-Za-z0-9_]+)\s*(!=|!~|=~|=)\s*("([^"\\]|\\.)*")/) if (!m) { throw new Error('Invalid label selector') } diff --git a/pyroscope/select_series.js b/pyroscope/select_series.js index 0cdeaa0a..0f406654 100644 --- a/pyroscope/select_series.js +++ b/pyroscope/select_series.js @@ -97,8 +97,8 @@ const selectSeriesImpl = async (fromTimeSec, toTimeSec, payload) => { typeIdSelector, serviceNameSelector ) - ).groupBy('timestamp_ns', 'fingerprint') - .orderBy(['fingerprint', 'ASC'], ['timestamp_ns', 'ASC']) + ).groupBy('timestamp_ms', 'fingerprint') + .orderBy(['fingerprint', 'ASC'], ['timestamp_ms', 'ASC']) const strMainReq = mainReq.toString() const chRes = await clickhouse .rawRequest(strMainReq + ' FORMAT JSON', null, DATABASE_NAME())