diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fe68c7c..0108d91 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -60,6 +60,11 @@ jobs: with: test_scenarios: python.* secrets: inherit + java: + uses: ./.github/workflows/test.yml + with: + test_scenarios: java.* + secrets: inherit full_host: uses: ./.github/workflows/test.yml with: diff --git a/analysis/analysis.go b/analysis/analysis.go index 6978b51..87b40c0 100644 --- a/analysis/analysis.go +++ b/analysis/analysis.go @@ -661,7 +661,7 @@ func AnalyzePprofFile(r Reporter, pprofFile string, typedStacks TypedStacks, tes } // AnalyzeResults loads the expected_profile.json at jsonFilePath and asserts -// every pprof file under pprofFolder matches it. Failures are reported via r. +// every profile file under pprofFolder matches it. Failures are reported via r. func AnalyzeResults(r Reporter, jsonFilePath string, pprofFolder string) { stackTestData, err := ReadJSONFile(jsonFilePath) if err != nil { diff --git a/analysis/jfr.go b/analysis/jfr.go new file mode 100644 index 0000000..6edaf63 --- /dev/null +++ b/analysis/jfr.go @@ -0,0 +1,153 @@ +// Package analysis — JFR support. +// +// This file adds the ability to read JFR (Java Flight Recorder) files and map +// JFR events directly into the analyzer's neutral ProfileSet. The mapping from +// event names to profile semantics intentionally lives here: prof-correctness is +// the consumer that knows which JDK / Datadog profiler events should satisfy a +// given expected_profile.json assertion. +package analysis + +import ( + "fmt" + "io" + "strings" + + "github.com/grafana/jfr-parser/parser" + "github.com/grafana/jfr-parser/parser/types" +) + +// FromJFR builds a ProfileSet directly from Java Flight Recorder events. +func FromJFR(data []byte) (*ProfileSet, error) { + p := parser.NewParser(data, parser.Options{ + SymbolProcessor: parser.ProcessSymbols, + }) + ps := newProfileSet() + + var cpuTotal int64 + var durationNanos uint64 + seenChunks := map[jfrChunkKey]bool{} + + for { + event, err := p.ParseRawEvent() + if err == io.EOF { + break + } + if err != nil { + return nil, fmt.Errorf("jfr ParseRawEvent: %w", err) + } + if event.Type == nil { + continue + } + + header := p.ChunkHeader() + chunk := jfrChunkKey{startNanos: header.StartNanos, durationNanos: header.DurationNanos} + if !seenChunks[chunk] { + seenChunks[chunk] = true + durationNanos += header.DurationNanos + } + + switch event.Type.Name { + case "jdk.ExecutionSample", "datadog.ExecutionSample": + fields, err := p.DecodeRawEventFields(event) + if err != nil { + return nil, fmt.Errorf("jfr decode %s: %w", event.Type.Name, err) + } + cpuTotal += addJFRCPU(ps, p, fields) + } + } + + if cpuTotal > 0 && durationNanos > 0 { + ps.addProfileDuration("cpu", cpuTotal, float64(durationNanos)/1e9) + } + + return ps.finalize(), nil +} + +type jfrChunkKey struct { + startNanos uint64 + durationNanos uint64 +} + +func addJFRCPU(ps *ProfileSet, p *parser.Parser, fields map[string]parser.RawField) int64 { + stackField, ok := jfrField(fields, "stackTrace") + if !ok { + return 0 + } + + // Match jfr-parser/pprof's CPU semantics: execution samples from sleeping + // threads do not count as CPU samples. If the state is absent, keep the + // sample rather than silently dropping producer-specific events. + if state, ok := jfrField(fields, "state"); ok { + if ts := p.GetThreadState(types.ThreadStateRef(state.Uint64)); ts != nil && ts.Name == "STATE_SLEEPING" { + return 0 + } + } + + folded := foldJFRStack(p, types.StackTraceRef(stackField.Uint64)) + if folded == "" { + return 0 + } + + val := int64(1) + if weight, ok := jfrField(fields, "weight"); ok && weight.Uint64 > 0 { + val = int64(weight.Uint64) + } + + ps.add("cpu", StackSample{Stack: folded, Val: val, Labels: executionSampleLabels(fields)}) + return val +} + +func executionSampleLabels(fields map[string]parser.RawField) map[string][]string { + labels := map[string][]string{} + if spanID, ok := jfrField(fields, "spanId"); ok && spanID.Uint64 != 0 { + labels[LabelSpanID] = []string{fmt.Sprintf("%d", spanID.Uint64)} + } + if localRootSpanID, ok := jfrField(fields, "localRootSpanId"); ok && localRootSpanID.Uint64 != 0 { + labels[LabelLocalRootSID] = []string{fmt.Sprintf("%d", localRootSpanID.Uint64)} + } + traceHi, traceHiOK := jfrField(fields, "traceIdHi") + traceLo, traceLoOK := jfrField(fields, "traceIdLo") + if (traceHiOK || traceLoOK) && (traceHi.Uint64 != 0 || traceLo.Uint64 != 0) { + labels[LabelTraceID] = []string{fmt.Sprintf("%016x%016x", traceHi.Uint64, traceLo.Uint64)} + } + return labels +} + +func jfrField(fields map[string]parser.RawField, name string) (parser.RawValue, bool) { + field, ok := fields[name] + if !ok { + return parser.RawValue{}, false + } + return field.First() +} + +func foldJFRStack(p *parser.Parser, stackRef types.StackTraceRef) string { + st := p.GetStacktrace(stackRef) + if st == nil || len(st.Frames) == 0 { + return "" + } + // JFR frames are leaf-first. The analyzer uses root-first folded stacks, + // matching FromPprof and the historical expected_profile.json captures. + frames := make([]string, 0, len(st.Frames)) + for i := len(st.Frames) - 1; i >= 0; i-- { + name := jfrFrameName(p, st.Frames[i].Method) + if name != "" { + frames = append(frames, name) + } + } + return strings.Join(frames, ";") +} + +func jfrFrameName(p *parser.Parser, methodRef types.MethodRef) string { + m := p.GetMethod(methodRef) + if m == nil { + return "" + } + methodName := p.GetSymbolString(m.Name) + cls := p.GetClass(m.Type) + if cls == nil { + return methodName + } + clsName := strings.ReplaceAll(p.GetSymbolString(cls.Name), "/", ".") + return clsName + "." + methodName +} diff --git a/analysis/model.go b/analysis/model.go index d8dcd1b..bee2871 100644 --- a/analysis/model.go +++ b/analysis/model.go @@ -24,6 +24,7 @@ package analysis import ( "path/filepath" "sort" + "strings" "github.com/google/pprof/profile" ) @@ -143,13 +144,18 @@ func (ps *ProfileSet) finalize() *ProfileSet { return ps } -// LoadProfileSet reads a profile file (pprof or OTLP) and returns the neutral -// ProfileSet. Format is chosen by filename suffix (.otlp/.otlp.pb -> OTLP +// isJFRName reports whether name should be parsed as a Java Flight Recorder recording. +func isJFRName(name string) bool { + return strings.HasSuffix(strings.ToLower(name), ".jfr") +} + +// LoadProfileSet reads a profile file (pprof, OTLP or JFR) and returns the neutral +// ProfileSet. Format is chosen by filename suffix (.jfr -> JFR, .otlp/.otlp.pb -> OTLP // proto, .otlp.json -> OTLP JSON, else pprof) with an OTLP fallback if pprof // parsing fails. Ambiguous suffixes such as .pb (used by both pprof and OTLP) // go through the content-based fallback rather than being forced to a format. // The per-format parsing lives in the respective adapter file (pprof.go / -// otlp.go). +// otlp.go / jfr.go). func LoadProfileSet(path string) (*ProfileSet, error) { content, err := readAndDecompress(path) if err != nil { @@ -158,6 +164,8 @@ func LoadProfileSet(path string) (*ProfileSet, error) { name := filepath.Base(path) switch { + case isJFRName(name): + return FromJFR(content) case isOTLPJSONName(name): return loadOTLP(content, true) case isOTLPProtoName(name): diff --git a/correctness_test.go b/correctness_test.go index 6d53a08..995c504 100644 --- a/correctness_test.go +++ b/correctness_test.go @@ -75,6 +75,10 @@ func TestDDProfScenarios(t *testing.T) { testScenarios(t, ".*ddprof.*") } +func TestJFRScenarios(t *testing.T) { + testScenarios(t, ".*jfr.*") +} + func TestPHPScenarios(t *testing.T) { testScenarios(t, ".*php.*") } diff --git a/go.mod b/go.mod index 68646b7..2e67d60 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,8 @@ module github.com/DataDog/prof-correctness go 1.25.1 require ( - github.com/google/pprof v0.0.0-20240528025155-186aa0362fba + github.com/google/pprof v0.0.0-20260507013755-92041b743c96 + github.com/grafana/jfr-parser v0.17.1 github.com/klauspost/compress v1.18.4 github.com/pierrec/lz4/v4 v4.1.25 github.com/xeipuuv/gojsonschema v1.2.0 @@ -22,8 +23,10 @@ require ( go.uber.org/multierr v1.11.0 // indirect golang.org/x/net v0.55.0 // indirect golang.org/x/sys v0.45.0 // indirect - golang.org/x/text v0.37.0 // indirect + golang.org/x/text v0.39.0 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20260414002931-afd174a4e478 // indirect google.golang.org/grpc v1.82.0 // indirect google.golang.org/protobuf v1.36.11 // indirect ) + +replace github.com/grafana/jfr-parser => github.com/r1viollet/pyroscope-jfr-parser v0.0.0-20260803080954-eddc025c5c4e diff --git a/go.sum b/go.sum index e75b789..77b7388 100644 --- a/go.sum +++ b/go.sum @@ -1,9 +1,22 @@ -github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -github.com/google/pprof v0.0.0-20240528025155-186aa0362fba h1:ql1qNgCyOB7iAEk8JTNM+zJrgIbnyCKX/wdlyPufP5g= -github.com/google/pprof v0.0.0-20240528025155-186aa0362fba/go.mod h1:K1liHPHnj73Fdn/EKuT8nrFqBihUSKXoLYU0BuatOYo= +github.com/google/pprof v0.0.0-20260507013755-92041b743c96 h1:YDDnaZ9afWajDboPMt9Vikqca/yWAX7KAxVzb4lJU1M= +github.com/google/pprof v0.0.0-20260507013755-92041b743c96/go.mod h1:MxpfABSjhmINe3F1It9d+8exIHFvUqtLIRCdOGNXqiI= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/hashicorp/go-version v1.9.0 h1:CeOIz6k+LoN3qX9Z0tyQrPtiB1DFYRPfCIBtaXPSCnA= github.com/hashicorp/go-version v1.9.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= @@ -18,34 +31,64 @@ github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee h1:W5t00kpgFd github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/pierrec/lz4/v4 v4.1.25 h1:kocOqRffaIbU5djlIBr7Wh+cx82C0vtFb0fOurZHqD0= github.com/pierrec/lz4/v4 v4.1.25/go.mod h1:EoQMVJgeeEOMsCqCzqFm2O0cJvljX2nGZjcRIPL34O4= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/r1viollet/pyroscope-jfr-parser v0.0.0-20260803080954-eddc025c5c4e h1:qOsPPgSArMHlR10zugkK6l5pCZp+8fK99pIcwxpYJ7M= +github.com/r1viollet/pyroscope-jfr-parser v0.0.0-20260803080954-eddc025c5c4e/go.mod h1:jNzwTrMNDUt6p/HcWjjtkTuZEfxUqr/gn/8hw0LaBJY= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f h1:J9EGpcZtP0E/raorCMxlFGSTBrsSlaDGf3jU/qvAE2c= github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0= github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74= github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= +go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= +go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= go.opentelemetry.io/collector/featuregate v1.62.0 h1:pYY7RlulSCTOS9mFWxasMLwYJCfNXHtnOkZlv3jg/V4= go.opentelemetry.io/collector/featuregate v1.62.0/go.mod h1:4ga1QBMPEejXXmpyJS8lmaRpknJ3Lb9Bvk6e420bUFU= +go.opentelemetry.io/collector/internal/testutil v0.156.0 h1:Nu02vhHA2UQ3Yjyjisk3N24HHxwvw7PQiTz9O1PuiUY= +go.opentelemetry.io/collector/internal/testutil v0.156.0/go.mod h1:Jkjs6rkqs973LqgZ0Fe3zrokQRKULYXPIf4HuqStiEE= go.opentelemetry.io/collector/pdata v1.62.0 h1:xGdwl2Cs5Rq5nKs0nYvAxm3Qq20HcySVAmUElATS8Es= go.opentelemetry.io/collector/pdata v1.62.0/go.mod h1:WFy5R6XGpz2Q4MaekeEm+qc4GY5V3+BhQIwGPkp+fj0= go.opentelemetry.io/collector/pdata/pprofile v0.156.0 h1:TnQzA2d5iMGH5//mGLqPjwdYqsFD/A7o2WgDdppxdVM= go.opentelemetry.io/collector/pdata/pprofile v0.156.0/go.mod h1:3dtjs/mliblJJCCTXUE0AkpBNfBEybPruj3ml6WCOoI= +go.opentelemetry.io/otel v1.44.0 h1:JjwHmHpA4iZ3wBxluu2fbbE7j4kqlE8jXyAyPXH7HqU= +go.opentelemetry.io/otel v1.44.0/go.mod h1:BMgjTHL9WPRlRjL2oZCBTL4whCGtXch2H4BhOPIAyYc= +go.opentelemetry.io/otel/metric v1.43.0 h1:d7638QeInOnuwOONPp4JAOGfbCEpYb+K6DVWvdxGzgM= +go.opentelemetry.io/otel/metric v1.43.0/go.mod h1:RDnPtIxvqlgO8GRW18W6Z/4P462ldprJtfxHxyKd2PY= +go.opentelemetry.io/otel/sdk v1.43.0 h1:pi5mE86i5rTeLXqoF/hhiBtUNcrAGHLKQdhg4h4V9Dg= +go.opentelemetry.io/otel/sdk v1.43.0/go.mod h1:P+IkVU3iWukmiit/Yf9AWvpyRDlUeBaRg6Y+C58QHzg= +go.opentelemetry.io/otel/sdk/metric v1.43.0 h1:S88dyqXjJkuBNLeMcVPRFXpRw2fuwdvfCGLEo89fDkw= +go.opentelemetry.io/otel/sdk/metric v1.43.0/go.mod h1:C/RJtwSEJ5hzTiUz5pXF1kILHStzb9zFlIEe85bhj6A= +go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09nk+3A= +go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0= +go.opentelemetry.io/proto/slim/otlp v1.10.0 h1:iR97Vs/ZDR+y9TfuP9b1XBtdPWeC+OMslIBmhcLU7jM= +go.opentelemetry.io/proto/slim/otlp v1.10.0/go.mod h1:lV9250stpjYLPNA5viFabIgP2QlUGRT1GdTgAf8SIUk= +go.opentelemetry.io/proto/slim/otlp/collector/profiles/v1development v0.3.0 h1:RUF5rO0hAlgiJt1fzQVzcVs3vZVNHIcMLgOgG4rWNcQ= +go.opentelemetry.io/proto/slim/otlp/collector/profiles/v1development v0.3.0/go.mod h1:I89cynRj8y+383o7tEQVg2SVA6SRgDVIouWPUVXjx0U= +go.opentelemetry.io/proto/slim/otlp/profiles/v1development v0.3.0 h1:CQvJSldHRUN6Z8jsUeYv8J0lXRvygALXIzsmAeCcZE0= +go.opentelemetry.io/proto/slim/otlp/profiles/v1development v0.3.0/go.mod h1:xSQ+mEfJe/GjK1LXEyVOoSI1N9JV9ZI923X5kup43W4= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= golang.org/x/net v0.55.0 h1:bcvxaJn3e1U6InsFWt1JUq1aSjnRxLzT2rtD2KfkDF8= golang.org/x/net v0.55.0/go.mod h1:L5U2KuzuOe1lY7Z+aWVIKK6qEeJXnXV9yzGA+WCHJww= golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY= golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= -golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc= -golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38= +golang.org/x/text v0.39.0 h1:UbZz4pLOvn600D6Oh6GGEI6VAmndrEBLv8/6BEXzyus= +golang.org/x/text v0.39.0/go.mod h1:3UwRclnC2g0TU9x8PZiyfOajCd1zaUNHF9cvqcQZ+ZM= +gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4= +gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E= google.golang.org/genproto/googleapis/rpc v0.0.0-20260414002931-afd174a4e478 h1:RmoJA1ujG+/lRGNfUnOMfhCy5EipVMyvUE+KNbPbTlw= google.golang.org/genproto/googleapis/rpc v0.0.0-20260414002931-afd174a4e478/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= google.golang.org/grpc v1.82.0 h1:vguDnZUPjE26w09A63VoxZPnvPjB5Riyc0mkXPFmAIU= google.golang.org/grpc v1.82.0/go.mod h1:yzTZ1TB1Z3SG+LIYaI+WiE8D5+PZ3ArnrSp8zF3+/ZA= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/scenarios/java_cpu_jfr/Dockerfile b/scenarios/java_cpu_jfr/Dockerfile new file mode 100644 index 0000000..dd3367f --- /dev/null +++ b/scenarios/java_cpu_jfr/Dockerfile @@ -0,0 +1,44 @@ +FROM eclipse-temurin:21-jdk + +WORKDIR /app + +RUN apt-get update \ + && apt-get install -y --no-install-recommends curl ca-certificates \ + && rm -rf /var/lib/apt/lists/* + +# Install the Datadog Java tracer, which carries the Datadog native Java +# profiler (ddprof). The profiler writes Datadog-specific JFR events such as +# datadog.ExecutionSample; this scenario validates that prof-correctness can +# consume those recordings directly. +RUN curl -fsSL -o /app/dd-java-agent.jar https://dtdg.co/latest-java-tracer + +COPY ./scenarios/java_cpu_jfr/DummyApp.java . +RUN javac DummyApp.java + +# EXECUTION_TIME_SEC is injected by the test harness; default to 60 s. +ENV EXECUTION_TIME_SEC=60 + +# Run the app under the Datadog Java profiler and use the tracer's built-in +# debug dump path to persist the JFR snapshot locally. The prof-correctness +# harness finds the timestamped dd-profiler-debug-*.jfr file by regex. +CMD sh -eu -c '\ + mkdir -p /app/data/dumps /app/data/tmp; \ + java \ + -javaagent:/app/dd-java-agent.jar \ + -Ddd.service=prof-correctness-java-cpu-jfr \ + -Ddd.env=local \ + -Ddd.trace.enabled=false \ + -Ddd.profiling.enabled=true \ + -Ddd.profiling.ddprof.enabled=true \ + -Ddd.profiling.ddprof.cpu.enabled=true \ + -Ddd.profiling.ddprof.wall.enabled=false \ + -Ddd.profiling.upload.period=30 \ + -Ddd.profiling.upload.timeout=1 \ + -Ddd.profiling.start-force-first=true \ + -Ddd.profiling.tempdir=/app/data/tmp \ + -Ddd.profiling.debug.dump_path=/app/data/dumps \ + -Ddd.profiling.url=http://127.0.0.1:8126/ \ + -Ddd.telemetry.enabled=false \ + -Ddd.jmxfetch.enabled=false \ + DummyApp \ +' diff --git a/scenarios/java_cpu_jfr/DummyApp.java b/scenarios/java_cpu_jfr/DummyApp.java new file mode 100644 index 0000000..4a5370c --- /dev/null +++ b/scenarios/java_cpu_jfr/DummyApp.java @@ -0,0 +1,33 @@ +/** + * Simple CPU-intensive app for JFR correctness testing. + * + * Continuously computes Fibonacci numbers to keep the CPU busy. + * The Datadog Java profiler is started by the JVM flags passed on the command line; + * this app just runs for the requested duration and exits. + * + * The EXECUTION_TIME_SEC environment variable (default: 30) controls how long + * the app runs, matching the convention used by prof-correctness. + */ +public class DummyApp { + public static void main(String[] args) throws InterruptedException { + int seconds = 30; + String env = System.getenv("EXECUTION_TIME_SEC"); + if (env != null && !env.isEmpty()) { + seconds = Integer.parseInt(env.trim()); + } + System.out.println("Running DummyApp for " + seconds + " seconds"); + + long endMs = System.currentTimeMillis() + seconds * 1000L; + int n = 40; // fib(40) takes ~0.4 s per call, keeps CPU fully busy + while (System.currentTimeMillis() < endMs) { + fibonacci(n); + } + System.out.println("DummyApp finished: " + fibonacci(n)); + } + + /** Intentionally naive recursive Fibonacci — the hot function we assert on. */ + static long fibonacci(int n) { + if (n <= 1) return n; + return fibonacci(n - 1) + fibonacci(n - 2); + } +} diff --git a/scenarios/java_cpu_jfr/expected_profile.json b/scenarios/java_cpu_jfr/expected_profile.json new file mode 100644 index 0000000..6093693 --- /dev/null +++ b/scenarios/java_cpu_jfr/expected_profile.json @@ -0,0 +1,17 @@ +{ + "test_name": "java_cpu_jfr", + "pprof-regex": "^dd-profiler-debug-.*\\.jfr$", + "stacks": [ + { + "profile-type": "cpu", + "stack-content": [ + { + "regular_expression": ".*DummyApp\\.fibonacci.*", + "percent": 95, + "error_margin": 15 + } + ] + } + ], + "scale_by_duration": false +}