Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
0651b82
adding Profiler
andimarek May 19, 2025
cc1de5e
progress
andimarek May 19, 2025
eef80ec
track running time
andimarek May 20, 2025
15cf642
tracking datafetcher result types
andimarek May 20, 2025
b7379ef
operation details
andimarek May 20, 2025
275eaac
wip
andimarek May 21, 2025
cd002c0
dataloader tracking
andimarek May 21, 2025
b75a14d
Merge branch 'master' into profiler-2
andimarek May 26, 2025
450a9fc
track dataloader dispatch
andimarek May 26, 2025
5e9e713
track dataloader dispatch
andimarek May 27, 2025
b3515b7
Merge branch 'master' into profiler-2
andimarek Jun 3, 2025
1800f77
Merge branch 'master' into profiler-2
andimarek Jun 30, 2025
221b416
non nullable handling
andimarek Jul 1, 2025
2ba4123
Merge branch 'master' into profiler-2
andimarek Jul 8, 2025
4008c11
master merging
andimarek Jul 9, 2025
f7e31e6
wip
andimarek Jul 16, 2025
3e29da4
Merge branch 'master' into profiler-2
andimarek Jul 16, 2025
999bb2f
handle non null case
andimarek Jul 16, 2025
5dde0e3
handle non null case
andimarek Jul 16, 2025
4a47b9d
add Map specific summary methods
andimarek Jul 17, 2025
0b7c15b
collect instrumentations
andimarek Jul 17, 2025
e6faa18
manual dispatch collected
andimarek Jul 18, 2025
0ab88da
wip
andimarek Jul 18, 2025
8bcdef8
wip
andimarek Jul 18, 2025
62f1e84
improve naming
andimarek Jul 18, 2025
76cdbc1
stabilize test
andimarek Jul 18, 2025
ca618b4
wip
andimarek Jul 18, 2025
535eb8e
counting wrapped trivial data fetchers
andimarek Jul 18, 2025
a04f5cb
introspection fields
andimarek Jul 19, 2025
572090e
data fetcher type statistics
andimarek Jul 19, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
track running time
  • Loading branch information
andimarek committed May 20, 2025
commit eef80ecbb53eb9b2f78f463190d6109d33f40a20
7 changes: 4 additions & 3 deletions src/main/java/graphql/EngineRunningState.java
Original file line number Diff line number Diff line change
Expand Up @@ -42,10 +42,11 @@ public EngineRunningState() {
this.executionId = null;
}

public EngineRunningState(ExecutionInput executionInput) {
public EngineRunningState(ExecutionInput executionInput, Profiler profiler) {
EngineRunningObserver engineRunningObserver = executionInput.getGraphQLContext().get(EngineRunningObserver.ENGINE_RUNNING_OBSERVER_KEY);
if (engineRunningObserver != null) {
this.engineRunningObserver = engineRunningObserver;
EngineRunningObserver wrappedObserver = profiler.wrapEngineRunningObserver(engineRunningObserver);
if (wrappedObserver != null) {
this.engineRunningObserver = wrappedObserver;
this.graphQLContext = executionInput.getGraphQLContext();
this.executionId = executionInput.getExecutionId();
} else {
Expand Down
4 changes: 2 additions & 2 deletions src/main/java/graphql/GraphQL.java
Original file line number Diff line number Diff line change
Expand Up @@ -421,10 +421,10 @@ public CompletableFuture<ExecutionResult> executeAsync(UnaryOperator<ExecutionIn
*/
public CompletableFuture<ExecutionResult> executeAsync(ExecutionInput executionInput) {
Profiler profiler = executionInput.isProfileExecution() ? new ProfilerImpl(executionInput.getGraphQLContext()) : Profiler.NO_OP;
profiler.start();
EngineRunningState engineRunningState = new EngineRunningState(executionInput);
EngineRunningState engineRunningState = new EngineRunningState(executionInput, profiler);
return engineRunningState.engineRun(() -> {
ExecutionInput executionInputWithId = ensureInputHasId(executionInput);
profiler.setExecutionId(executionInputWithId.getExecutionId());
engineRunningState.updateExecutionId(executionInputWithId.getExecutionId());

CompletableFuture<InstrumentationState> instrumentationStateCF = instrumentation.createStateAsync(new InstrumentationCreateStateParameters(this.graphQLSchema, executionInputWithId));
Expand Down
11 changes: 11 additions & 0 deletions src/main/java/graphql/Profiler.java
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
package graphql;

import graphql.execution.EngineRunningObserver;
import graphql.execution.ExecutionId;
import graphql.execution.ResultPath;
import graphql.schema.DataFetcher;
import org.jspecify.annotations.NullMarked;
import org.jspecify.annotations.Nullable;

@Internal
@NullMarked
Expand All @@ -25,7 +28,15 @@ default void subSelectionCount(int size) {

}

default void setExecutionId(ExecutionId executionId) {

}

default void fieldFetched(Object fetchedObject, DataFetcher<?> dataFetcher, ResultPath path) {

}

default @Nullable EngineRunningObserver wrapEngineRunningObserver(EngineRunningObserver engineRunningObserver) {
return engineRunningObserver;
}
}
45 changes: 42 additions & 3 deletions src/main/java/graphql/ProfilerImpl.java
Original file line number Diff line number Diff line change
@@ -1,16 +1,23 @@
package graphql;

import graphql.execution.EngineRunningObserver;
import graphql.execution.ExecutionId;
import graphql.execution.ResultPath;
import graphql.schema.DataFetcher;
import graphql.schema.PropertyDataFetcher;
import graphql.schema.SingletonPropertyDataFetcher;
import org.jspecify.annotations.NullMarked;

import java.util.concurrent.atomic.AtomicLong;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not for this PR, but in the future, with more Profiler features you might want to introduce options to toggle sets of tracking on and off

e.g. toggle DataLoader related tracking on and off

@Internal
@NullMarked
public class ProfilerImpl implements Profiler {

volatile long startTime;
private volatile long startTime;
private volatile long endTime;
private volatile long lastStartTime;
private final AtomicLong engineTotalRunningTime = new AtomicLong();


final ProfilerResult profilerResult = new ProfilerResult();
Expand All @@ -20,8 +27,8 @@ public ProfilerImpl(GraphQLContext graphQLContext) {
}

@Override
public void start() {
startTime = System.nanoTime();
public void setExecutionId(ExecutionId executionId) {
profilerResult.setExecutionId(executionId);
}

@Override
Expand All @@ -37,4 +44,36 @@ public void fieldFetched(Object fetchedObject, DataFetcher<?> dataFetcher, Resul
}
profilerResult.setDataFetcherType(key, dataFetcherType);
}

@Override
public EngineRunningObserver wrapEngineRunningObserver(EngineRunningObserver engineRunningObserver) {
// nothing to wrap here
return new EngineRunningObserver() {
@Override
public void runningStateChanged(ExecutionId executionId, GraphQLContext graphQLContext, RunningState runningState) {
runningStateChangedImpl(executionId, graphQLContext, runningState);
if (engineRunningObserver != null) {
engineRunningObserver.runningStateChanged(executionId, graphQLContext, runningState);
}
}
};
}

private void runningStateChangedImpl(ExecutionId executionId, GraphQLContext graphQLContext, EngineRunningObserver.RunningState runningState) {
long now = System.nanoTime();
if (runningState == EngineRunningObserver.RunningState.RUNNING_START) {
startTime = now;
lastStartTime = startTime;
} else if (runningState == EngineRunningObserver.RunningState.NOT_RUNNING_FINISH) {
endTime = now;
engineTotalRunningTime.set(engineTotalRunningTime.get() + (endTime - lastStartTime));
profilerResult.setTimes(startTime, endTime, engineTotalRunningTime.get());
} else if (runningState == EngineRunningObserver.RunningState.RUNNING) {
lastStartTime = now;
} else if (runningState == EngineRunningObserver.RunningState.NOT_RUNNING) {
engineTotalRunningTime.set(engineTotalRunningTime.get() + (now - lastStartTime));
} else {
Assert.assertShouldNeverHappen();
}
}
}
55 changes: 52 additions & 3 deletions src/main/java/graphql/ProfilerResult.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package graphql;

import graphql.execution.ExecutionId;

import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Set;
Expand All @@ -9,16 +11,22 @@
@ExperimentalApi
public class ProfilerResult {

public static String PROFILER_CONTEXT_KEY = "__GJ_PROFILER";
public static final String PROFILER_CONTEXT_KEY = "__GJ_PROFILER";

private volatile ExecutionId executionId;
private long startTime;
private long endTime;
private long engineTotalRunningTime;
private final Set<String> fieldsFetched = ConcurrentHashMap.newKeySet();

private final AtomicInteger totalDataFetcherInvocations = new AtomicInteger();
private final AtomicInteger totalPropertyDataFetcherInvocations = new AtomicInteger();


private final Set<String> fieldsFetched = ConcurrentHashMap.newKeySet();
private final Map<String, Integer> dataFetcherInvocationCount = new ConcurrentHashMap<>();
private final Map<String, DataFetcherType> dataFetcherTypeMap = new ConcurrentHashMap<>();


public enum DataFetcherType {
PROPERTY_DATA_FETCHER,
CUSTOM
Expand All @@ -31,7 +39,8 @@ public enum ResultType {

}

private Map<String, ResultType> queryPathToResultType;

// setters are package private to prevent exposure

void setDataFetcherType(String key, DataFetcherType dataFetcherType) {
dataFetcherTypeMap.putIfAbsent(key, dataFetcherType);
Expand All @@ -49,6 +58,16 @@ void addFieldFetched(String fieldPath) {
fieldsFetched.add(fieldPath);
}

void setExecutionId(ExecutionId executionId) {
this.executionId = executionId;
}

void setTimes(long startTime, long endTime, long engineTotalRunningTime) {
this.startTime = startTime;
this.endTime = endTime;
this.engineTotalRunningTime = engineTotalRunningTime;
}


public Set<String> getFieldsFetched() {
return fieldsFetched;
Expand Down Expand Up @@ -87,4 +106,34 @@ public int getTotalCustomDataFetcherInvocations() {
return totalDataFetcherInvocations.get() - totalPropertyDataFetcherInvocations.get();
}

public long getStartTime() {
return startTime;
}

public long getEndTime() {
return endTime;
}

public long getEngineTotalRunningTime() {
return engineTotalRunningTime;
}

public long getTotalExecutionTime() {
return endTime - startTime;
}

@Override
public String toString() {
return "ProfilerResult{" +
"executionId=" + executionId +
", startTime=" + startTime +
", endTime=" + endTime +
", engineTotalRunningTime=" + engineTotalRunningTime +
", fieldsFetched=" + fieldsFetched +
", totalDataFetcherInvocations=" + totalDataFetcherInvocations +
", totalPropertyDataFetcherInvocations=" + totalPropertyDataFetcherInvocations +
", dataFetcherInvocationCount=" + dataFetcherInvocationCount +
", dataFetcherTypeMap=" + dataFetcherTypeMap +
'}';
}
}
52 changes: 52 additions & 0 deletions src/test/groovy/graphql/ProfilerTest.groovy
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ import graphql.schema.DataFetcher
import graphql.schema.DataFetchingEnvironment
import spock.lang.Specification

import java.time.Duration
import java.util.concurrent.CompletableFuture

class ProfilerTest extends Specification {


Expand Down Expand Up @@ -74,4 +77,53 @@ class ProfilerTest extends Specification {
profilerResult.getTotalPropertyDataFetcherInvocations() == 3
}

def "records timing"() {
given:
def sdl = '''
type Query {
foo: Foo
}
type Foo {
id: String
}
'''
def schema = TestUtil.schema(sdl, [
Query: [
foo: { DataFetchingEnvironment dfe ->
return CompletableFuture.supplyAsync {
Thread.sleep(500)
"1"
}
} as DataFetcher],
Foo : [
id: { DataFetchingEnvironment dfe ->
return CompletableFuture.supplyAsync {
Thread.sleep(500)
dfe.source
}
} as DataFetcher
]])
def graphql = GraphQL.newGraphQL(schema).build();

ExecutionInput ei = ExecutionInput.newExecutionInput()
.query("{ foo { id } }")
.profileExecution(true)
.build()

when:
def result = graphql.execute(ei)
def profilerResult = ei.getGraphQLContext().get(ProfilerResult.PROFILER_CONTEXT_KEY) as ProfilerResult

then:
result.getData() == [foo: [id: "1"]]
// the total execution time must be more than 1 second,
// the engine should take less than 500ms
profilerResult.getTotalExecutionTime() > Duration.ofSeconds(1).toNanos()
profilerResult.getEngineTotalRunningTime() > Duration.ofMillis(1).toNanos()
profilerResult.getEngineTotalRunningTime() < Duration.ofMillis(500).toNanos()


}


Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

extra line - intentional?

}
8 changes: 4 additions & 4 deletions src/test/groovy/graphql/execution/ExecutionTest.groovy
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ class ExecutionTest extends Specification {
def document = parser.parseDocument(query)

when:
execution.execute(document, MutationSchema.schema, ExecutionId.generate(), emptyExecutionInput, instrumentationState, new EngineRunningState(emptyExecutionInput), Profiler.NO_OP)
execution.execute(document, MutationSchema.schema, ExecutionId.generate(), emptyExecutionInput, instrumentationState, new EngineRunningState(emptyExecutionInput, Profiler.NO_OP), Profiler.NO_OP)

then:
queryStrategy.execute == 1
Expand All @@ -73,7 +73,7 @@ class ExecutionTest extends Specification {
def document = parser.parseDocument(query)

when:
execution.execute(document, MutationSchema.schema, ExecutionId.generate(), emptyExecutionInput, instrumentationState, new EngineRunningState(emptyExecutionInput), Profiler.NO_OP)
execution.execute(document, MutationSchema.schema, ExecutionId.generate(), emptyExecutionInput, instrumentationState, new EngineRunningState(emptyExecutionInput, Profiler.NO_OP), Profiler.NO_OP)

then:
queryStrategy.execute == 0
Expand All @@ -93,7 +93,7 @@ class ExecutionTest extends Specification {
def document = parser.parseDocument(query)

when:
execution.execute(document, MutationSchema.schema, ExecutionId.generate(), emptyExecutionInput, instrumentationState, new EngineRunningState(emptyExecutionInput), Profiler.NO_OP)
execution.execute(document, MutationSchema.schema, ExecutionId.generate(), emptyExecutionInput, instrumentationState, new EngineRunningState(emptyExecutionInput, Profiler.NO_OP), Profiler.NO_OP)

then:
queryStrategy.execute == 0
Expand Down Expand Up @@ -130,7 +130,7 @@ class ExecutionTest extends Specification {


when:
execution.execute(document, MutationSchema.schema, ExecutionId.generate(), emptyExecutionInput, instrumentationState, new EngineRunningState(emptyExecutionInput), Profiler.NO_OP)
execution.execute(document, MutationSchema.schema, ExecutionId.generate(), emptyExecutionInput, instrumentationState, new EngineRunningState(emptyExecutionInput, Profiler.NO_OP), Profiler.NO_OP)

then:
queryStrategy.execute == 0
Expand Down