From 946468dbf42ff4450edc94762812ddb4a5f3482d Mon Sep 17 00:00:00 2001 From: eduardo aleixo Date: Sun, 24 Apr 2022 02:38:34 -0300 Subject: [PATCH] fix(jfr): fixes a parser regression introduced in 1.15.0 (#1050) * chore: add a java-jfr example * use JFR * fixes a bug, adds tests * makes units and AggregationType proper types * moves adhoc-pull to a separate directory so that linters doesn't complain * fix * fixes * fix * fix Co-authored-by: Dmitry Filimonov --- benchmark/internal/loadgen/loadgen.go | 5 +- examples/adhoc/{ => pull}/adhoc-pull.go | 0 examples/java-jfr/Dockerfile | 26 ++++++++ examples/java-jfr/Main.java | 51 +++++++++++++++ examples/java-jfr/docker-compose.yml | 19 ++++++ examples/java/Dockerfile | 8 +-- go.mod | 2 +- go.sum | 4 +- .../FlameGraph/FlameGraphComponent/Header.tsx | 4 +- .../pyroscope-flamegraph/src/format/format.ts | 55 ++++++++++++++++ packages/pyroscope-models/src/flamebearer.ts | 8 ++- packages/pyroscope-models/src/profile.ts | 10 ++- pkg/adhoc/external_writer.go | 4 +- pkg/adhoc/server/convert.go | 3 +- pkg/agent/spy/spy.go | 16 ++--- pkg/agent/target/target.go | 2 +- pkg/agent/upstream/remote/remote.go | 4 +- pkg/agent/upstream/remote/remote_test.go | 3 +- pkg/agent/upstream/upstream.go | 5 +- pkg/convert/jfr/parser.go | 59 +++++++++--------- pkg/convert/pprof/writer.go | 4 +- pkg/parser/parser.go | 15 ++++- pkg/scrape/config/config.go | 15 ++--- pkg/server/ingest.go | 11 ++-- pkg/server/ingest_test.go | 51 +++++++++++++++ pkg/server/render.go | 3 +- .../testdata/jfr-alloc_in_new_tlab_bytes.txt | 2 + .../jfr-alloc_in_new_tlab_objects.txt | 2 + .../testdata/jfr-alloc_outside_tlab_bytes.txt | 1 + .../jfr-alloc_outside_tlab_objects.txt | 1 + pkg/server/testdata/jfr-cpu.txt | 16 +++++ pkg/server/testdata/jfr-lock_count.txt | 1 + pkg/server/testdata/jfr-lock_duration.txt | 1 + pkg/server/testdata/jfr.bin.gz | Bin 0 -> 29391 bytes pkg/storage/metadata/metadata.go | 18 ++++++ pkg/storage/segment/segment.go | 12 ++-- pkg/storage/segment/serialization.go | 19 +++--- pkg/storage/storage_get.go | 3 +- pkg/storage/storage_put.go | 5 +- pkg/storage/tree/pprof.go | 20 +++--- pkg/structs/flamebearer/flamebearer.go | 3 +- pkg/structs/flamebearer/flamebearer_test.go | 3 +- pkg/testing/load/app.go | 13 ++-- 43 files changed, 401 insertions(+), 106 deletions(-) rename examples/adhoc/{ => pull}/adhoc-pull.go (100%) create mode 100644 examples/java-jfr/Dockerfile create mode 100644 examples/java-jfr/Main.java create mode 100644 examples/java-jfr/docker-compose.yml create mode 100644 pkg/server/testdata/jfr-alloc_in_new_tlab_bytes.txt create mode 100644 pkg/server/testdata/jfr-alloc_in_new_tlab_objects.txt create mode 100644 pkg/server/testdata/jfr-alloc_outside_tlab_bytes.txt create mode 100644 pkg/server/testdata/jfr-alloc_outside_tlab_objects.txt create mode 100644 pkg/server/testdata/jfr-cpu.txt create mode 100644 pkg/server/testdata/jfr-lock_count.txt create mode 100644 pkg/server/testdata/jfr-lock_duration.txt create mode 100644 pkg/server/testdata/jfr.bin.gz create mode 100644 pkg/storage/metadata/metadata.go diff --git a/benchmark/internal/loadgen/loadgen.go b/benchmark/internal/loadgen/loadgen.go index 5451c40766..4c34a2d064 100644 --- a/benchmark/internal/loadgen/loadgen.go +++ b/benchmark/internal/loadgen/loadgen.go @@ -17,6 +17,7 @@ import ( "github.com/pyroscope-io/pyroscope/benchmark/internal/server" "github.com/pyroscope-io/pyroscope/pkg/agent/upstream" "github.com/pyroscope-io/pyroscope/pkg/agent/upstream/remote" + "github.com/pyroscope-io/pyroscope/pkg/storage/metadata" "github.com/pyroscope-io/pyroscope/pkg/structs/transporttrie" "github.com/sirupsen/logrus" ) @@ -262,8 +263,8 @@ Outside: EndTime: et, SpyName: "gospy", SampleRate: 100, - Units: "samples", - AggregationType: "sum", + Units: metadata.SamplesUnits, + AggregationType: metadata.SumAggregationType, Trie: t, }) l.pauseMutex.RUnlock() diff --git a/examples/adhoc/adhoc-pull.go b/examples/adhoc/pull/adhoc-pull.go similarity index 100% rename from examples/adhoc/adhoc-pull.go rename to examples/adhoc/pull/adhoc-pull.go diff --git a/examples/java-jfr/Dockerfile b/examples/java-jfr/Dockerfile new file mode 100644 index 0000000000..e14ef59fd0 --- /dev/null +++ b/examples/java-jfr/Dockerfile @@ -0,0 +1,26 @@ +FROM openjdk:17-slim-bullseye + +WORKDIR /opt/app + +RUN apt-get update && apt-get install ca-certificates -y && update-ca-certificates && apt-get install -y git +RUN git clone https://github.com/pyroscope-io/pyroscope-java.git && \ + cd pyroscope-java && \ + git checkout v0.6.0 && \ + ./gradlew shadowJar && \ + cp agent/build/libs/pyroscope.jar /opt/app/pyroscope.jar + +COPY Main.java ./ + +RUN javac Main.java + +ENV PYROSCOPE_APPLICATION_NAME=fibonacci.java.push.app +ENV PYROSCOPE_FORMAT=jfr +ENV PYROSCOPE_PROFILING_INTERVAL=10ms +ENV PYROSCOPE_PROFILER_EVENT=cpu +ENV PYROSCOPE_PROFILER_LOCK=1 +ENV PYROSCOPE_PROFILER_ALLOC=1 +ENV PYROSCOPE_UPLOAD_INTERVAL=10s +ENV PYROSCOPE_LOG_LEVEL=debug +ENV PYROSCOPE_SERVER_ADDRESS=http://localhost:4040 + +CMD ["java", "-XX:-Inline", "-javaagent:pyroscope.jar", "Main"] diff --git a/examples/java-jfr/Main.java b/examples/java-jfr/Main.java new file mode 100644 index 0000000000..9e8b2a1400 --- /dev/null +++ b/examples/java-jfr/Main.java @@ -0,0 +1,51 @@ +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; + +class MyRunnable implements Runnable { + private Lock lock; + + public MyRunnable(Lock lock) { + this.lock = lock; + } + + public static long fib(long n) { + if (n < 2) + return n; + return fib(n-1) + fib(n-2); + } + + public void run() { + while (true) { + this.lock.lock(); + try { + fib(40); + } finally { + this.lock.unlock(); + } + } + } +} + +class Main { + public static long fib(long n) { + if (n < 2) + return n; + return fib(n-1) + fib(n-2); + } + + public static void main(String[] args) { + Lock l = new ReentrantLock(); + Runnable r = new MyRunnable(l); + new Thread(r).start(); + + while (true) { + l.lock(); + try { + fib(40); + } finally { + l.unlock(); + } + } + } +} + diff --git a/examples/java-jfr/docker-compose.yml b/examples/java-jfr/docker-compose.yml new file mode 100644 index 0000000000..b4ea2c69ca --- /dev/null +++ b/examples/java-jfr/docker-compose.yml @@ -0,0 +1,19 @@ +--- +version: '3.9' +services: + pyroscope: + image: 'pyroscope/pyroscope:latest' + ports: + - '4040:4040' + command: + - 'server' + - '-no-self-profiling' + app: + build: . + privileged: true + environment: + - 'PYROSCOPE_APPLICATION_NAME=fibonacci-java-lock-push' + - 'PYROSCOPE_PROFILER_EVENT=lock' + - 'PYROSCOPE_SERVER_ADDRESS=http://pyroscope:4040' + - 'PYROSCOPE_FORMAT=jfr' + - 'PYROSCOPE_PROFILER_LOCK=0' diff --git a/examples/java/Dockerfile b/examples/java/Dockerfile index 322a870fff..d479ecb055 100644 --- a/examples/java/Dockerfile +++ b/examples/java/Dockerfile @@ -1,12 +1,14 @@ FROM openjdk:11.0.11-jdk WORKDIR /opt/app -RUN git clone https://github.com/pyroscope-io/pyroscope-java.git -RUN cd pyroscope-java && \ +RUN git clone https://github.com/pyroscope-io/pyroscope-java.git && \ + cd pyroscope-java && \ + git checkout v0.6.0 && \ ./gradlew shadowJar && \ cp agent/build/libs/pyroscope.jar /opt/app/pyroscope.jar COPY Main.java ./Main.java +RUN javac Main.java ENV PYROSCOPE_APPLICATION_NAME=simple.java.app ENV PYROSCOPE_PROFILING_INTERVAL=10ms @@ -15,6 +17,4 @@ ENV PYROSCOPE_UPLOAD_INTERVAL=10s ENV PYROSCOPE_LOG_LEVEL=debug ENV PYROSCOPE_SERVER_ADDRESS=http://pyroscope:4040 -RUN javac Main.java - CMD ["java", "-XX:-Inline", "-javaagent:pyroscope.jar", "Main"] diff --git a/go.mod b/go.mod index d00ad47b91..2cd657c986 100644 --- a/go.mod +++ b/go.mod @@ -47,7 +47,7 @@ require ( github.com/pyroscope-io/client v0.0.0-20211206204731-3fd0a4b8239c github.com/pyroscope-io/dotnetdiag v1.2.1 github.com/pyroscope-io/goldga v0.4.2-0.20220218190441-817afcc3a7f1 - github.com/pyroscope-io/jfr-parser v0.4.0 + github.com/pyroscope-io/jfr-parser v0.4.1 github.com/rlmcpherson/s3gof3r v0.5.0 github.com/shirou/gopsutil v3.21.4+incompatible github.com/sirupsen/logrus v1.8.1 diff --git a/go.sum b/go.sum index f05cdc8f70..c0ab3322cf 100644 --- a/go.sum +++ b/go.sum @@ -618,8 +618,8 @@ github.com/pyroscope-io/dotnetdiag v1.2.1 h1:3XEMrfFJnZ87BiEhozyQKmCUAuMd/Spq7KC github.com/pyroscope-io/dotnetdiag v1.2.1/go.mod h1:eFUEHCp4eD1TgcXMlJihC+R4MrqGf7nTRdWxNADbDHA= github.com/pyroscope-io/goldga v0.4.2-0.20220218190441-817afcc3a7f1 h1:T1fDdt3E3UpaGZ7tRF2IYrUFwNyuPlIWGeCOjfINp1s= github.com/pyroscope-io/goldga v0.4.2-0.20220218190441-817afcc3a7f1/go.mod h1:PbX5bxlj/WxyKIEAxYgNMNWUpXP4rt9GqtjfvTf8m+I= -github.com/pyroscope-io/jfr-parser v0.4.0 h1:K5daiLhW4XP2miyCAlw67hqpd7WSo4ROVc/DXSfpECk= -github.com/pyroscope-io/jfr-parser v0.4.0/go.mod h1:ZMcbJjfDkOwElEK8CvUJbpetztRWRXszCmf5WU0erV8= +github.com/pyroscope-io/jfr-parser v0.4.1 h1:lYFQHIQC3YkjlQhqf5fsObyW7sxAjc6NsFodOMNg9js= +github.com/pyroscope-io/jfr-parser v0.4.1/go.mod h1:ZMcbJjfDkOwElEK8CvUJbpetztRWRXszCmf5WU0erV8= github.com/pyroscope-io/revive v1.0.6-0.20210330033039-4a71146f9dc1 h1:0v9lBNgdmVtpyyk9PP/DfpJlOHkXriu5YgNlrhQw5YE= github.com/pyroscope-io/revive v1.0.6-0.20210330033039-4a71146f9dc1/go.mod h1:tSw34BaGZ0iF+oVKDOjq1/LuxGifgW7shaJ6+dBYFXg= github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= diff --git a/packages/pyroscope-flamegraph/src/FlameGraph/FlameGraphComponent/Header.tsx b/packages/pyroscope-flamegraph/src/FlameGraph/FlameGraphComponent/Header.tsx index a5eca30472..6c1b3afcc5 100644 --- a/packages/pyroscope-flamegraph/src/FlameGraph/FlameGraphComponent/Header.tsx +++ b/packages/pyroscope-flamegraph/src/FlameGraph/FlameGraphComponent/Header.tsx @@ -20,9 +20,11 @@ export default function Header(props: HeaderProps) { const { format, units, ExportData = <>, palette, setPalette } = props; const unitsToFlamegraphTitle = { - objects: 'amount of objects in RAM per function', + objects: 'number of objects in RAM per function', bytes: 'amount of RAM per function', samples: 'CPU time per function', + lock_nanoseconds: 'time spent waiting on locks per function', + lock_samples: 'number of contended locks per function', '': '', }; diff --git a/packages/pyroscope-flamegraph/src/format/format.ts b/packages/pyroscope-flamegraph/src/format/format.ts index 3d134364d0..2b900566f6 100644 --- a/packages/pyroscope-flamegraph/src/format/format.ts +++ b/packages/pyroscope-flamegraph/src/format/format.ts @@ -22,6 +22,10 @@ export function getFormatter(max: number, sampleRate: number, unit: Units) { return new ObjectsFormatter(max); case 'bytes': return new BytesFormatter(max); + case 'lock_nanoseconds': + return new NanosecondsFormatter(max); + case 'lock_samples': + return new ObjectsFormatter(max); default: console.warn(`Unsupported unit: '${unit}'. Defaulting to 'samples'`); return new DurationFormatter(max / sampleRate); @@ -77,6 +81,57 @@ class DurationFormatter { } } +// this is a class and not a function because we can save some time by +// precalculating divider and suffix and not doing it on each iteration +class NanosecondsFormatter { + divider = 1; + multiplier = 1; + + suffix: string = 'second'; + + durations: [number, string][] = [ + [60, 'minute'], + [60, 'hour'], + [24, 'day'], + [30, 'month'], + [12, 'year'], + ]; + + constructor(maxDur: number) { + maxDur /= 1000000000; + // eslint-disable-next-line no-plusplus + for (let i = 0; i < this.durations.length; i++) { + const level = this.durations[i]; + if (!level) { + console.warn('Could not calculate level'); + break; + } + + if (maxDur >= level[0]) { + this.divider *= level[0]; + maxDur /= level[0]; + // eslint-disable-next-line prefer-destructuring + this.suffix = level[1]; + } else { + break; + } + } + } + + format(samples: number, sampleRate: number) { + const n = samples / 1000000000 / this.divider; + let nStr = n.toFixed(2); + + if (n >= 0 && n < 0.01) { + nStr = '< 0.01'; + } else if (n <= 0 && n > -0.01) { + nStr = '< 0.01'; + } + + return `${nStr} ${this.suffix}${n === 1 ? '' : 's'}`; + } +} + export class ObjectsFormatter { divider = 1; diff --git a/packages/pyroscope-models/src/flamebearer.ts b/packages/pyroscope-models/src/flamebearer.ts index 622227db45..8ea8f40493 100644 --- a/packages/pyroscope-models/src/flamebearer.ts +++ b/packages/pyroscope-models/src/flamebearer.ts @@ -20,7 +20,13 @@ export type Flamebearer = { * Sample Rate, used in text information */ sampleRate: number; - units: 'samples' | 'objects' | 'bytes' | ''; + units: + | 'samples' + | 'objects' + | 'bytes' + | 'lock_samples' + | 'lock_nanoseconds' + | ''; spyName: | 'dotneyspy' | 'ebpfspy' diff --git a/packages/pyroscope-models/src/profile.ts b/packages/pyroscope-models/src/profile.ts index b5661bacbe..b6abefbba5 100644 --- a/packages/pyroscope-models/src/profile.ts +++ b/packages/pyroscope-models/src/profile.ts @@ -10,14 +10,20 @@ export const FlamebearerSchema = z.object({ // accept the defined units // and convert anything else to empty string const UnitsSchema = z.preprocess((u) => { - const units = ['samples', 'objects', 'bytes']; + const units = [ + 'samples', + 'objects', + 'bytes', + 'lock_samples', + 'lock_nanoseconds', + ]; if (typeof u === 'string') { if (units.includes(u)) { return u; } } return ''; -}, z.enum(['samples', 'objects', 'bytes', ''])); +}, z.enum(['samples', 'objects', 'bytes', 'lock_samples', 'lock_nanoseconds', ''])); export const MetadataSchema = z.object({ // Optional fields since adhoc may be missing them diff --git a/pkg/adhoc/external_writer.go b/pkg/adhoc/external_writer.go index c4485eb494..d8214d93a9 100644 --- a/pkg/adhoc/external_writer.go +++ b/pkg/adhoc/external_writer.go @@ -80,7 +80,9 @@ func (w *externalWriter) write(name string, out *storage.GetOutput) error { switch w.format { case "pprof": pprof := out.Tree.Pprof(&tree.PprofMetadata{ - Unit: out.Units, + // TODO(petethepig): check if this conversion always makes sense + // e.g are these units defined in pprof somewhere? + Unit: string(out.Units), StartTime: w.now, }) out, err := proto.Marshal(pprof) diff --git a/pkg/adhoc/server/convert.go b/pkg/adhoc/server/convert.go index b7f3c432a7..67fa1d4bed 100644 --- a/pkg/adhoc/server/convert.go +++ b/pkg/adhoc/server/convert.go @@ -12,6 +12,7 @@ import ( "github.com/pyroscope-io/pyroscope/pkg/agent/spy" "github.com/pyroscope-io/pyroscope/pkg/convert" "github.com/pyroscope-io/pyroscope/pkg/storage" + "github.com/pyroscope-io/pyroscope/pkg/storage/metadata" "github.com/pyroscope-io/pyroscope/pkg/storage/tree" "github.com/pyroscope-io/pyroscope/pkg/structs/flamebearer" ) @@ -38,7 +39,7 @@ func PprofToProfileV1(b []byte, name string, maxNodes int) (*flamebearer.Flamebe // TODO(abeaumont): Support multiple sample types for _, stype := range p.SampleTypes() { sampleRate := uint32(100) - units := "samples" + units := metadata.SamplesUnits if c, ok := tree.DefaultSampleTypeMapping[stype]; ok { units = c.Units if c.Sampled && p.Period > 0 { diff --git a/pkg/agent/spy/spy.go b/pkg/agent/spy/spy.go index f24b04bc12..5116f85276 100644 --- a/pkg/agent/spy/spy.go +++ b/pkg/agent/spy/spy.go @@ -3,6 +3,8 @@ package spy import ( "fmt" + + "github.com/pyroscope-io/pyroscope/pkg/storage/metadata" ) type Spy interface { @@ -32,23 +34,23 @@ func (t ProfileType) IsCumulative() bool { return t == ProfileAllocObjects || t == ProfileAllocSpace } -func (t ProfileType) Units() string { +func (t ProfileType) Units() metadata.Units { if t == ProfileInuseObjects || t == ProfileAllocObjects { - return "objects" + return metadata.ObjectsUnits } if t == ProfileInuseSpace || t == ProfileAllocSpace { - return "bytes" + return metadata.BytesUnits } - return "samples" + return metadata.SamplesUnits } -func (t ProfileType) AggregationType() string { +func (t ProfileType) AggregationType() metadata.AggregationType { if t == ProfileInuseObjects || t == ProfileInuseSpace { - return "average" + return metadata.AverageAggregationType } - return "sum" + return metadata.SumAggregationType } // TODO: this interface is not the best as different spies have different arguments diff --git a/pkg/agent/target/target.go b/pkg/agent/target/target.go index b3d2380981..7491aa9c07 100644 --- a/pkg/agent/target/target.go +++ b/pkg/agent/target/target.go @@ -73,7 +73,7 @@ func (mgr *Manager) canonise(t *config.Target) error { t.SampleRate = types.DefaultSampleRate } if t.ApplicationName == "" { - t.ApplicationName = t.SpyName + "." + names.GetRandomName(generateSeed(t.ServiceName, t.SpyName)) + t.ApplicationName = string(t.SpyName) + "." + names.GetRandomName(generateSeed(t.ServiceName, t.SpyName)) logger := mgr.logger.WithField("spy-name", t.SpyName) if t.ServiceName != "" { logger = logger.WithField("service-name", t.ServiceName) diff --git a/pkg/agent/upstream/remote/remote.go b/pkg/agent/upstream/remote/remote.go index 8563e42537..cb0e917860 100644 --- a/pkg/agent/upstream/remote/remote.go +++ b/pkg/agent/upstream/remote/remote.go @@ -110,8 +110,8 @@ func (r *Remote) uploadProfile(j *upstream.UploadJob) error { q.Set("until", strconv.Itoa(int(j.EndTime.Unix()))) q.Set("spyName", j.SpyName) q.Set("sampleRate", strconv.Itoa(int(j.SampleRate))) - q.Set("units", j.Units) - q.Set("aggregationType", j.AggregationType) + q.Set("units", string(j.Units)) + q.Set("aggregationType", string(j.AggregationType)) u.Path = path.Join(u.Path, "/ingest") u.RawQuery = q.Encode() diff --git a/pkg/agent/upstream/remote/remote_test.go b/pkg/agent/upstream/remote/remote_test.go index 739e719dc9..d616cd7674 100644 --- a/pkg/agent/upstream/remote/remote_test.go +++ b/pkg/agent/upstream/remote/remote_test.go @@ -12,6 +12,7 @@ import ( . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" "github.com/pyroscope-io/pyroscope/pkg/agent/upstream" + "github.com/pyroscope-io/pyroscope/pkg/storage/metadata" "github.com/pyroscope-io/pyroscope/pkg/structs/transporttrie" "github.com/pyroscope-io/pyroscope/pkg/testing" "github.com/sirupsen/logrus" @@ -61,7 +62,7 @@ var _ = Describe("remote.Remote", func() { EndTime: testing.SimpleTime(10), SpyName: "debugspy", SampleRate: 100, - Units: "samples", + Units: metadata.SamplesUnits, Trie: t, }) } diff --git a/pkg/agent/upstream/upstream.go b/pkg/agent/upstream/upstream.go index ec8e4c131b..bca725cc49 100644 --- a/pkg/agent/upstream/upstream.go +++ b/pkg/agent/upstream/upstream.go @@ -3,6 +3,7 @@ package upstream import ( "time" + "github.com/pyroscope-io/pyroscope/pkg/storage/metadata" "github.com/pyroscope-io/pyroscope/pkg/structs/transporttrie" ) @@ -12,8 +13,8 @@ type UploadJob struct { EndTime time.Time SpyName string SampleRate uint32 - Units string - AggregationType string + Units metadata.Units + AggregationType metadata.AggregationType Trie *transporttrie.Trie } diff --git a/pkg/convert/jfr/parser.go b/pkg/convert/jfr/parser.go index 531fa2342a..938f1223e5 100644 --- a/pkg/convert/jfr/parser.go +++ b/pkg/convert/jfr/parser.go @@ -9,6 +9,7 @@ import ( "github.com/pyroscope-io/jfr-parser/parser" "github.com/pyroscope-io/pyroscope/pkg/storage" + "github.com/pyroscope-io/pyroscope/pkg/storage/metadata" "github.com/pyroscope-io/pyroscope/pkg/storage/segment" "github.com/pyroscope-io/pyroscope/pkg/storage/tree" ) @@ -93,9 +94,9 @@ func parse(ctx context.Context, c parser.Chunk, s storage.Putter, pi *storage.Pu labels["__name__"] = prefix + "." + profile pi.Key = segment.NewKey(labels) pi.Val = cpu - pi.Units = "samples" - pi.AggregationType = "sum" - if putErr := s.Put(ctx, pi); err != nil { + pi.Units = metadata.SamplesUnits + pi.AggregationType = metadata.SumAggregationType + if putErr := s.Put(ctx, pi); putErr != nil { err = multierror.Append(err, putErr) } } @@ -103,9 +104,9 @@ func parse(ctx context.Context, c parser.Chunk, s storage.Putter, pi *storage.Pu labels["__name__"] = prefix + "." + event pi.Key = segment.NewKey(labels) pi.Val = wall - pi.Units = "samples" - pi.AggregationType = "sum" - if putErr := s.Put(ctx, pi); err != nil { + pi.Units = metadata.SamplesUnits + pi.AggregationType = metadata.SumAggregationType + if putErr := s.Put(ctx, pi); putErr != nil { err = multierror.Append(err, putErr) } } @@ -113,51 +114,51 @@ func parse(ctx context.Context, c parser.Chunk, s storage.Putter, pi *storage.Pu labels["__name__"] = prefix + ".alloc_in_new_tlab_objects" pi.Key = segment.NewKey(labels) pi.Val = inTLABObjects - pi.Units = "objects" - pi.AggregationType = "sum" - if putErr := s.Put(ctx, pi); err != nil { + pi.Units = metadata.ObjectsUnits + pi.AggregationType = metadata.SumAggregationType + if putErr := s.Put(ctx, pi); putErr != nil { err = multierror.Append(err, putErr) } labels["__name__"] = prefix + ".alloc_in_new_tlab_bytes" pi.Key = segment.NewKey(labels) - pi.Val = inTLABObjects - pi.Units = "bytes" - pi.AggregationType = "sum" - if putErr := s.Put(ctx, pi); err != nil { + pi.Val = inTLABBytes + pi.Units = metadata.BytesUnits + pi.AggregationType = metadata.SumAggregationType + if putErr := s.Put(ctx, pi); putErr != nil { err = multierror.Append(err, putErr) } labels["__name__"] = prefix + ".alloc_outside_tlab_objects" pi.Key = segment.NewKey(labels) - pi.Val = inTLABObjects - pi.Units = "objects" - pi.AggregationType = "sum" - if putErr := s.Put(ctx, pi); err != nil { + pi.Val = outTLABObjects + pi.Units = metadata.ObjectsUnits + pi.AggregationType = metadata.SumAggregationType + if putErr := s.Put(ctx, pi); putErr != nil { err = multierror.Append(err, putErr) } labels["__name__"] = prefix + ".alloc_outside_tlab_bytes" pi.Key = segment.NewKey(labels) - pi.Val = inTLABObjects - pi.Units = "bytes" - pi.AggregationType = "sum" - if putErr := s.Put(ctx, pi); err != nil { + pi.Val = outTLABBytes + pi.Units = metadata.BytesUnits + pi.AggregationType = metadata.SumAggregationType + if putErr := s.Put(ctx, pi); putErr != nil { err = multierror.Append(err, putErr) } } if lock != "" { - labels["__name__"] = prefix + ".lock_samples" + labels["__name__"] = prefix + ".lock_count" pi.Key = segment.NewKey(labels) pi.Val = lockSamples - pi.Units = "samples" - pi.AggregationType = "sum" - if putErr := s.Put(ctx, pi); err != nil { + pi.Units = metadata.LockSamplesUnits + pi.AggregationType = metadata.SumAggregationType + if putErr := s.Put(ctx, pi); putErr != nil { err = multierror.Append(err, putErr) } - labels["__name__"] = prefix + ".lock_nanoseconds" + labels["__name__"] = prefix + ".lock_duration" pi.Key = segment.NewKey(labels) pi.Val = lockDuration - pi.Units = "nanoseconds" - pi.AggregationType = "sum" - if putErr := s.Put(ctx, pi); err != nil { + pi.Units = metadata.LockNanosecondsUnits + pi.AggregationType = metadata.SumAggregationType + if putErr := s.Put(ctx, pi); putErr != nil { err = multierror.Append(err, putErr) } } diff --git a/pkg/convert/pprof/writer.go b/pkg/convert/pprof/writer.go index 0832bff710..effd434915 100644 --- a/pkg/convert/pprof/writer.go +++ b/pkg/convert/pprof/writer.go @@ -6,6 +6,7 @@ import ( "time" "github.com/pyroscope-io/pyroscope/pkg/storage" + "github.com/pyroscope-io/pyroscope/pkg/storage/metadata" "github.com/pyroscope-io/pyroscope/pkg/storage/segment" "github.com/pyroscope-io/pyroscope/pkg/storage/tree" ) @@ -80,7 +81,8 @@ func (w *ProfileWriter) WriteProfile(ctx context.Context, startTime, endTime tim if sampleTypeConfig.Units != "" { pi.Units = sampleTypeConfig.Units } else { - pi.Units = p.StringTable[vt.Unit] + // TODO(petethepig): this conversion is questionable + pi.Units = metadata.Units(p.StringTable[vt.Unit]) } pi.Key = w.buildName(sampleType, p.ResolveLabels(l)) w.ingester.Enqueue(ctx, &pi) diff --git a/pkg/parser/parser.go b/pkg/parser/parser.go index 100c12663e..025479a644 100644 --- a/pkg/parser/parser.go +++ b/pkg/parser/parser.go @@ -13,6 +13,7 @@ import ( "github.com/pyroscope-io/pyroscope/pkg/convert/jfr" "github.com/pyroscope-io/pyroscope/pkg/convert/pprof" "github.com/pyroscope-io/pyroscope/pkg/storage" + "github.com/pyroscope-io/pyroscope/pkg/storage/metadata" "github.com/pyroscope-io/pyroscope/pkg/storage/segment" "github.com/pyroscope-io/pyroscope/pkg/storage/tree" "github.com/pyroscope-io/pyroscope/pkg/structs/transporttrie" @@ -37,8 +38,8 @@ type PutInput struct { Key *segment.Key SpyName string SampleRate uint32 - Units string - AggregationType string + Units metadata.Units + AggregationType metadata.AggregationType } type Parser struct { @@ -82,6 +83,16 @@ func (p *Parser) Put(ctx context.Context, in *PutInput) (err error, pErr error) AggregationType: in.AggregationType, } cb := p.createParseCallback(pi) + + // for tests (ingest_test.go): + // b, _ := io.ReadAll(in.Body) + // f, _ := os.Create("./pkg/server/testdata/jfr-" + strconv.Itoa(i) + ".bin.gz") + // i++ + // w := gzip.NewWriter(f) + // w.Write(b) + // w.Close() + // in.Body = bytes.NewReader(b) + switch { case in.Format == "trie", in.ContentType == "binary/octet-stream+trie": tmpBuf := p.bufferPool.Get() diff --git a/pkg/scrape/config/config.go b/pkg/scrape/config/config.go index 9413bfd4d6..6f916b69e0 100644 --- a/pkg/scrape/config/config.go +++ b/pkg/scrape/config/config.go @@ -26,6 +26,7 @@ import ( "github.com/pyroscope-io/pyroscope/pkg/scrape/discovery" "github.com/pyroscope-io/pyroscope/pkg/scrape/relabel" + "github.com/pyroscope-io/pyroscope/pkg/storage/metadata" profile "github.com/pyroscope-io/pyroscope/pkg/storage/tree" "github.com/pyroscope-io/pyroscope/pkg/util/bytesize" ) @@ -47,7 +48,7 @@ func DefaultConfig() *Config { SampleTypes: map[string]*profile.SampleTypeConfig{ "samples": { DisplayName: "cpu", - Units: "samples", + Units: metadata.SamplesUnits, Sampled: true, }, }, @@ -57,19 +58,19 @@ func DefaultConfig() *Config { Params: nil, // url.Values{"gc": []string{"1"}}, SampleTypes: map[string]*profile.SampleTypeConfig{ "inuse_objects": { - Units: "objects", - Aggregation: "average", + Units: metadata.ObjectsUnits, + Aggregation: metadata.AverageAggregationType, }, "alloc_objects": { - Units: "objects", + Units: metadata.ObjectsUnits, Cumulative: true, }, "inuse_space": { - Units: "bytes", - Aggregation: "average", + Units: metadata.BytesUnits, + Aggregation: metadata.AverageAggregationType, }, "alloc_space": { - Units: "bytes", + Units: metadata.BytesUnits, Cumulative: true, }, }, diff --git a/pkg/server/ingest.go b/pkg/server/ingest.go index c42a3f2658..ea362c55a8 100644 --- a/pkg/server/ingest.go +++ b/pkg/server/ingest.go @@ -13,6 +13,7 @@ import ( "github.com/pyroscope-io/pyroscope/pkg/agent/types" "github.com/pyroscope-io/pyroscope/pkg/parser" "github.com/pyroscope-io/pyroscope/pkg/server/httputils" + "github.com/pyroscope-io/pyroscope/pkg/storage/metadata" "github.com/pyroscope-io/pyroscope/pkg/storage/segment" "github.com/pyroscope-io/pyroscope/pkg/util/attime" ) @@ -119,15 +120,17 @@ func (h ingestHandler) ingestParamsFromRequest(r *http.Request) (*parser.PutInpu } if u := q.Get("units"); u != "" { - pi.Units = u + // TODO(petethepig): add validation for these? + pi.Units = metadata.Units(u) } else { - pi.Units = "samples" + pi.Units = metadata.SamplesUnits } if at := q.Get("aggregationType"); at != "" { - pi.AggregationType = at + // TODO(petethepig): add validation for these? + pi.AggregationType = metadata.AggregationType(at) } else { - pi.AggregationType = "sum" + pi.AggregationType = metadata.SumAggregationType } return &pi, nil diff --git a/pkg/server/ingest_test.go b/pkg/server/ingest_test.go index b2e235cc9f..c739908db5 100644 --- a/pkg/server/ingest_test.go +++ b/pkg/server/ingest_test.go @@ -5,6 +5,7 @@ import ( "context" "encoding/json" "fmt" + "io" "io/ioutil" "mime/multipart" "net/http" @@ -13,6 +14,7 @@ import ( "strconv" "time" + "github.com/klauspost/compress/gzip" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" "github.com/prometheus/client_golang/prometheus" @@ -33,6 +35,16 @@ func readTestdataFile(name string) string { return string(f) } +func jfrFromFile(name string) *bytes.Buffer { + b, err := ioutil.ReadFile(name) + Expect(err).ToNot(HaveOccurred()) + b2, err := gzip.NewReader(bytes.NewBuffer(b)) + Expect(err).ToNot(HaveOccurred()) + b3, err := io.ReadAll(b2) + Expect(err).ToNot(HaveOccurred()) + return bytes.NewBuffer(b3) +} + func pprofFormFromFile(name string, cfg map[string]*tree.SampleTypeConfig) (*multipart.Writer, *bytes.Buffer) { b, err := ioutil.ReadFile(name) Expect(err).ToNot(HaveOccurred()) @@ -63,6 +75,7 @@ var _ = Describe("server", func() { Describe("/ingest", func() { var buf *bytes.Buffer var format string + // var typeName string var contentType string var name string var sleepDur time.Duration @@ -139,6 +152,10 @@ var _ = Describe("server", func() { Expect(gOut).ToNot(BeNil()) Expect(err).ToNot(HaveOccurred()) Expect(gOut.Tree).ToNot(BeNil()) + // Useful for debugging + // fmt.Println("sk ", sk) + // fmt.Println(gOut.Tree.String()) + // ioutil.WriteFile("/home/dmitry/pyroscope/pkg/server/testdata/jfr-"+typeName+".txt", []byte(gOut.Tree.String()), 0644) Expect(gOut.Tree.String()).To(Equal(expectedTree)) close(done) @@ -218,6 +235,40 @@ var _ = Describe("server", func() { ItCorrectlyParsesIncomingData() }) + Context("jfr", func() { + BeforeEach(func() { + format = "" + sleepDur = 100 * time.Millisecond + name = "test.app{foo=bar,baz=qux}" + buf = jfrFromFile("./testdata/jfr.bin.gz") + format = "jfr" + }) + + types := []string{ + "cpu", + "alloc_in_new_tlab_objects", + "alloc_in_new_tlab_bytes", + "alloc_outside_tlab_objects", + "alloc_outside_tlab_bytes", + "lock_count", + "lock_duration", + } + + for _, t := range types { + func(t string) { + Context(t, func() { + BeforeEach(func() { + // typeName = t + expectedKey = "test.app." + t + "{foo=bar,baz=qux}" + expectedTree = readTestdataFile("./testdata/jfr-" + t + ".txt") + }) + + ItCorrectlyParsesIncomingData() + }) + }(t) + } + }) + Context("pprof", func() { BeforeEach(func() { format = "" diff --git a/pkg/server/render.go b/pkg/server/render.go index 9613a3a9af..e0392c96d4 100644 --- a/pkg/server/render.go +++ b/pkg/server/render.go @@ -114,7 +114,8 @@ func (rh *RenderHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { rh.httpUtils.WriteResponseJSON(r, w, res) case "pprof": pprof := out.Tree.Pprof(&tree.PprofMetadata{ - Unit: out.Units, + // TODO(petethepig): not sure if this conversion is right + Unit: string(out.Units), StartTime: p.gi.StartTime, }) out, err := proto.Marshal(pprof) diff --git a/pkg/server/testdata/jfr-alloc_in_new_tlab_bytes.txt b/pkg/server/testdata/jfr-alloc_in_new_tlab_bytes.txt new file mode 100644 index 0000000000..81e5c2426a --- /dev/null +++ b/pkg/server/testdata/jfr-alloc_in_new_tlab_bytes.txt @@ -0,0 +1,2 @@ +.no_Java_frame 503680 +java/lang/Thread.run;io/pyroscope/javaagent/Uploader.run;io/pyroscope/javaagent/Uploader.uploadSnapshot;java/net/HttpURLConnection.getResponseCode;sun/net/www/protocol/http/HttpURLConnection.getInputStream;sun/net/www/protocol/http/HttpURLConnection.getInputStream0;sun/net/www/http/HttpClient.parseHTTP;java/io/BufferedInputStream.;java/io/BufferedInputStream. 511520 diff --git a/pkg/server/testdata/jfr-alloc_in_new_tlab_objects.txt b/pkg/server/testdata/jfr-alloc_in_new_tlab_objects.txt new file mode 100644 index 0000000000..5c9788c248 --- /dev/null +++ b/pkg/server/testdata/jfr-alloc_in_new_tlab_objects.txt @@ -0,0 +1,2 @@ +.no_Java_frame 1 +java/lang/Thread.run;io/pyroscope/javaagent/Uploader.run;io/pyroscope/javaagent/Uploader.uploadSnapshot;java/net/HttpURLConnection.getResponseCode;sun/net/www/protocol/http/HttpURLConnection.getInputStream;sun/net/www/protocol/http/HttpURLConnection.getInputStream0;sun/net/www/http/HttpClient.parseHTTP;java/io/BufferedInputStream.;java/io/BufferedInputStream. 1 diff --git a/pkg/server/testdata/jfr-alloc_outside_tlab_bytes.txt b/pkg/server/testdata/jfr-alloc_outside_tlab_bytes.txt new file mode 100644 index 0000000000..8d7c3dec5a --- /dev/null +++ b/pkg/server/testdata/jfr-alloc_outside_tlab_bytes.txt @@ -0,0 +1 @@ +java/lang/Thread.run;io/pyroscope/javaagent/Uploader.run;io/pyroscope/javaagent/Uploader.uploadSnapshot;io/pyroscope/javaagent/Uploader.sendRequest;java/io/FilterOutputStream.write;java/io/DataOutputStream.write;sun/net/www/http/PosterOutputStream.write;java/io/ByteArrayOutputStream.write;java/io/ByteArrayOutputStream.ensureCapacity;java/util/Arrays.copyOf 148248 diff --git a/pkg/server/testdata/jfr-alloc_outside_tlab_objects.txt b/pkg/server/testdata/jfr-alloc_outside_tlab_objects.txt new file mode 100644 index 0000000000..8f532bda77 --- /dev/null +++ b/pkg/server/testdata/jfr-alloc_outside_tlab_objects.txt @@ -0,0 +1 @@ +java/lang/Thread.run;io/pyroscope/javaagent/Uploader.run;io/pyroscope/javaagent/Uploader.uploadSnapshot;io/pyroscope/javaagent/Uploader.sendRequest;java/io/FilterOutputStream.write;java/io/DataOutputStream.write;sun/net/www/http/PosterOutputStream.write;java/io/ByteArrayOutputStream.write;java/io/ByteArrayOutputStream.ensureCapacity;java/util/Arrays.copyOf 1 diff --git a/pkg/server/testdata/jfr-cpu.txt b/pkg/server/testdata/jfr-cpu.txt new file mode 100644 index 0000000000..ae222a9d60 --- /dev/null +++ b/pkg/server/testdata/jfr-cpu.txt @@ -0,0 +1,16 @@ +Main.main;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib 2 +Main.main;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib 1 +Main.main;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib 5 +Main.main;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib 6 +Main.main;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib 14 +Main.main;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib 29 +Main.main;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib 54 +Main.main;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib 95 +Main.main;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib 155 +Main.main;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib 173 +Main.main;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib 166 +Main.main;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib 148 +Main.main;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib 89 +Main.main;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib 40 +Main.main;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib 13 +Main.main;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib;Main.fib 6 diff --git a/pkg/server/testdata/jfr-lock_count.txt b/pkg/server/testdata/jfr-lock_count.txt new file mode 100644 index 0000000000..41c399e210 --- /dev/null +++ b/pkg/server/testdata/jfr-lock_count.txt @@ -0,0 +1 @@ +java/lang/Thread.run;MyRunnable.run;java/util/concurrent/locks/ReentrantLock.lock;java/util/concurrent/locks/ReentrantLock$Sync.lock;java/util/concurrent/locks/AbstractQueuedSynchronizer.acquire;java/util/concurrent/locks/AbstractQueuedSynchronizer.acquire;java/util/concurrent/locks/LockSupport.park;jdk/internal/misc/Unsafe.park 27 diff --git a/pkg/server/testdata/jfr-lock_duration.txt b/pkg/server/testdata/jfr-lock_duration.txt new file mode 100644 index 0000000000..883fe15dd3 --- /dev/null +++ b/pkg/server/testdata/jfr-lock_duration.txt @@ -0,0 +1 @@ +java/lang/Thread.run;MyRunnable.run;java/util/concurrent/locks/ReentrantLock.lock;java/util/concurrent/locks/ReentrantLock$Sync.lock;java/util/concurrent/locks/AbstractQueuedSynchronizer.acquire;java/util/concurrent/locks/AbstractQueuedSynchronizer.acquire;java/util/concurrent/locks/LockSupport.park;jdk/internal/misc/Unsafe.park 9960666236 diff --git a/pkg/server/testdata/jfr.bin.gz b/pkg/server/testdata/jfr.bin.gz new file mode 100644 index 0000000000000000000000000000000000000000..54559b0914dc5a56501861b130db78d33cf61b91 GIT binary patch literal 29391 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 atk%`HH8qWD`u_j`0RR7G0i);MMgjoWvpF9C literal 0 HcmV?d00001 diff --git a/pkg/storage/metadata/metadata.go b/pkg/storage/metadata/metadata.go new file mode 100644 index 0000000000..8486a8e69d --- /dev/null +++ b/pkg/storage/metadata/metadata.go @@ -0,0 +1,18 @@ +package metadata + +type Units string + +const ( + SamplesUnits Units = "samples" + ObjectsUnits = "objects" + BytesUnits = "bytes" + LockNanosecondsUnits = "lock_nanoseconds" + LockSamplesUnits = "lock_samples" +) + +type AggregationType string + +const ( + AverageAggregationType AggregationType = "average" + SumAggregationType AggregationType = "sum" +) diff --git a/pkg/storage/segment/segment.go b/pkg/storage/segment/segment.go index cfcbf4c266..bb9186035a 100644 --- a/pkg/storage/segment/segment.go +++ b/pkg/storage/segment/segment.go @@ -10,6 +10,8 @@ import ( "runtime/trace" "sync" "time" + + "github.com/pyroscope-io/pyroscope/pkg/storage/metadata" ) type streeNode struct { @@ -227,8 +229,8 @@ type Segment struct { spyName string sampleRate uint32 - units string - aggregationType string + units metadata.Units + aggregationType metadata.AggregationType watermarks } @@ -409,7 +411,7 @@ func (s *Segment) WalkNodesToDelete(t *RetentionPolicy, cb func(depth int, t tim } // TODO: this should be refactored -func (s *Segment) SetMetadata(spyName string, sampleRate uint32, units, aggregationType string) { +func (s *Segment) SetMetadata(spyName string, sampleRate uint32, units metadata.Units, aggregationType metadata.AggregationType) { s.spyName = spyName s.sampleRate = sampleRate s.units = units @@ -424,11 +426,11 @@ func (s *Segment) SampleRate() uint32 { return s.sampleRate } -func (s *Segment) Units() string { +func (s *Segment) Units() metadata.Units { return s.units } -func (s *Segment) AggregationType() string { +func (s *Segment) AggregationType() metadata.AggregationType { return s.aggregationType } diff --git a/pkg/storage/segment/serialization.go b/pkg/storage/segment/serialization.go index 69202435b5..d51e6161b4 100644 --- a/pkg/storage/segment/serialization.go +++ b/pkg/storage/segment/serialization.go @@ -7,6 +7,7 @@ import ( "io" "time" + "github.com/pyroscope-io/pyroscope/pkg/storage/metadata" "github.com/pyroscope-io/pyroscope/pkg/util/serialization" "github.com/pyroscope-io/pyroscope/pkg/util/varint" ) @@ -14,18 +15,18 @@ import ( // serialization format version. it's not very useful right now, but it will be in the future const currentVersion = 3 -func (s *Segment) populateFromMetadata(metadata map[string]interface{}) { - if v, ok := metadata["sampleRate"]; ok { +func (s *Segment) populateFromMetadata(mdata map[string]interface{}) { + if v, ok := mdata["sampleRate"]; ok { s.sampleRate = uint32(v.(float64)) } - if v, ok := metadata["spyName"]; ok { + if v, ok := mdata["spyName"]; ok { s.spyName = v.(string) } - if v, ok := metadata["units"]; ok { - s.units = v.(string) + if v, ok := mdata["units"]; ok { + s.units = metadata.Units(v.(string)) } - if v, ok := metadata["aggregationType"]; ok { - s.aggregationType = v.(string) + if v, ok := mdata["aggregationType"]; ok { + s.aggregationType = metadata.AggregationType(v.(string)) } } @@ -101,12 +102,12 @@ func Deserialize(r io.Reader) (*Segment, error) { return nil, err } - metadata, err := serialization.ReadMetadata(br) + mdata, err := serialization.ReadMetadata(br) if err != nil { return nil, err } - s.populateFromMetadata(metadata) + s.populateFromMetadata(mdata) parents := []*streeNode{nil} for len(parents) > 0 { diff --git a/pkg/storage/storage_get.go b/pkg/storage/storage_get.go index d18f2485b5..8ae852d0ac 100644 --- a/pkg/storage/storage_get.go +++ b/pkg/storage/storage_get.go @@ -11,6 +11,7 @@ import ( "github.com/pyroscope-io/pyroscope/pkg/flameql" "github.com/pyroscope-io/pyroscope/pkg/storage/dimension" + "github.com/pyroscope-io/pyroscope/pkg/storage/metadata" "github.com/pyroscope-io/pyroscope/pkg/storage/segment" "github.com/pyroscope-io/pyroscope/pkg/storage/tree" ) @@ -27,7 +28,7 @@ type GetOutput struct { Timeline *segment.Timeline SpyName string SampleRate uint32 - Units string + Units metadata.Units } const ( diff --git a/pkg/storage/storage_put.go b/pkg/storage/storage_put.go index b2e27d6e82..6834aec46f 100644 --- a/pkg/storage/storage_put.go +++ b/pkg/storage/storage_put.go @@ -9,6 +9,7 @@ import ( "github.com/sirupsen/logrus" "github.com/pyroscope-io/pyroscope/pkg/storage/dimension" + "github.com/pyroscope-io/pyroscope/pkg/storage/metadata" "github.com/pyroscope-io/pyroscope/pkg/storage/segment" "github.com/pyroscope-io/pyroscope/pkg/storage/tree" ) @@ -20,8 +21,8 @@ type PutInput struct { Val *tree.Tree SpyName string SampleRate uint32 - Units string - AggregationType string + Units metadata.Units + AggregationType metadata.AggregationType } func (s *Storage) Put(_ context.Context, pi *PutInput) error { diff --git a/pkg/storage/tree/pprof.go b/pkg/storage/tree/pprof.go index 72c30f069f..d314763626 100644 --- a/pkg/storage/tree/pprof.go +++ b/pkg/storage/tree/pprof.go @@ -2,14 +2,16 @@ package tree import ( "time" + + "github.com/pyroscope-io/pyroscope/pkg/storage/metadata" ) type SampleTypeConfig struct { - Units string `json:"units,omitempty" yaml:"units,omitempty"` - DisplayName string `json:"display-name,omitempty" yaml:"display-name,omitempty"` - Aggregation string `json:"aggregation,omitempty" yaml:"aggregation,omitempty"` - Cumulative bool `json:"cumulative,omitempty" yaml:"cumulative,omitempty"` - Sampled bool `json:"sampled,omitempty" yaml:"sampled,omitempty"` + Units metadata.Units `json:"units,omitempty" yaml:"units,omitempty"` + DisplayName string `json:"display-name,omitempty" yaml:"display-name,omitempty"` + Aggregation metadata.AggregationType `json:"aggregation,omitempty" yaml:"aggregation,omitempty"` + Cumulative bool `json:"cumulative,omitempty" yaml:"cumulative,omitempty"` + Sampled bool `json:"sampled,omitempty" yaml:"sampled,omitempty"` } // DefaultSampleTypeMapping contains default settings for every @@ -88,7 +90,7 @@ type PprofMetadata struct { Duration time.Duration } -func (t *Tree) Pprof(metadata *PprofMetadata) *Profile { +func (t *Tree) Pprof(mdata *PprofMetadata) *Profile { t.RLock() defer t.RUnlock() @@ -101,9 +103,9 @@ func (t *Tree) Pprof(metadata *PprofMetadata) *Profile { }, } - p.profile.SampleType = []*ValueType{{Type: p.newString(metadata.Type), Unit: p.newString(metadata.Unit)}} - p.profile.TimeNanos = metadata.StartTime.UnixNano() - p.profile.DurationNanos = metadata.Duration.Nanoseconds() + p.profile.SampleType = []*ValueType{{Type: p.newString(mdata.Type), Unit: p.newString(mdata.Unit)}} + p.profile.TimeNanos = mdata.StartTime.UnixNano() + p.profile.DurationNanos = mdata.Duration.Nanoseconds() t.IterateStacks(func(name string, self uint64, stack []string) { value := []int64{int64(self)} loc := []uint64{} diff --git a/pkg/structs/flamebearer/flamebearer.go b/pkg/structs/flamebearer/flamebearer.go index b269f485d5..4099bf15a7 100644 --- a/pkg/structs/flamebearer/flamebearer.go +++ b/pkg/structs/flamebearer/flamebearer.go @@ -5,6 +5,7 @@ import ( "fmt" "github.com/pyroscope-io/pyroscope/pkg/storage" + "github.com/pyroscope-io/pyroscope/pkg/storage/metadata" "github.com/pyroscope-io/pyroscope/pkg/storage/segment" "github.com/pyroscope-io/pyroscope/pkg/storage/tree" ) @@ -80,7 +81,7 @@ type FlamebearerMetadataV1 struct { // Sample rate at which the profiler was operating. SampleRate uint32 `json:"sampleRate"` // The unit of measurement for the profiled data. - Units string `json:"units"` + Units metadata.Units `json:"units"` // A name that identifies the profile. Name string `json:"name"` } diff --git a/pkg/structs/flamebearer/flamebearer_test.go b/pkg/structs/flamebearer/flamebearer_test.go index 885a6a993f..d9d1a2bd6e 100644 --- a/pkg/structs/flamebearer/flamebearer_test.go +++ b/pkg/structs/flamebearer/flamebearer_test.go @@ -5,6 +5,7 @@ import ( . "github.com/onsi/gomega" "github.com/pyroscope-io/pyroscope/pkg/storage" + "github.com/pyroscope-io/pyroscope/pkg/storage/metadata" "github.com/pyroscope-io/pyroscope/pkg/storage/segment" "github.com/pyroscope-io/pyroscope/pkg/storage/tree" ) @@ -17,7 +18,7 @@ var ( maxNodes = 1024 spyName = "spy-name" sampleRate = uint32(10) - units = "units" + units = metadata.Units("units") ) var _ = Describe("FlamebearerProfile", func() { diff --git a/pkg/testing/load/app.go b/pkg/testing/load/app.go index e19e27faf0..b5a2865229 100644 --- a/pkg/testing/load/app.go +++ b/pkg/testing/load/app.go @@ -4,6 +4,7 @@ import ( "time" "github.com/pyroscope-io/pyroscope/pkg/storage" + "github.com/pyroscope-io/pyroscope/pkg/storage/metadata" "github.com/pyroscope-io/pyroscope/pkg/storage/segment" ) @@ -11,18 +12,18 @@ type App struct { Name string SpyName string SampleRate uint32 - Units string - AggregationType string + Units metadata.Units + AggregationType metadata.AggregationType tags *TagsGenerator trees *TreeGenerator } type AppConfig struct { - SpyName string `yaml:"spyName"` - SampleRate uint32 `yaml:"sampleRate"` - Units string `yaml:"units"` - AggregationType string `yaml:"aggregationType"` + SpyName string `yaml:"spyName"` + SampleRate uint32 `yaml:"sampleRate"` + Units metadata.Units `yaml:"units"` + AggregationType metadata.AggregationType `yaml:"aggregationType"` Tags []Tag `yaml:"tags"` Trees int `yaml:"trees"`