Skip to content

Testing performance (bean tracking)

Tracker records guice bean methods execution:

  1. Collect method call arguments and result for each call
  2. Log slow methods execution
  3. Collect metrics to show overall methods performance (stats)

Warning

Trackers will not work for HK2 beans and for non guice-managed beans (bound by instance)

Note

Initially, trackers were added as a simpler alternative for mockito spy's clumsy result capturing. But, eventually, it evolved into a simple performance tracking tool (very raw, of course, but in many cases it would be enough).

Setup

Not strictly required, but trackers provide type-safe search api using mockito, and so you'll need mockito dependency only if you wish to use this api (version may be omitted if dropwizard BOM used):

testImplementation 'org.mockito:mockito-core'

Usage

Suppose we have a service:

public static class Service {

    public String get(int id) {
        return "Hello " + id;
    }
}

And we want to very indirect service call (when service called by some other service):

TrackersHook hook = TrackersHook();
final Tracker<Service> tracker = hook.track(Service.class)
        .trace(true)
        .add();
TestSupport.build(DefaultTestApp .class)
        .hooks(hook)
        .runCore(injector ->{
            Service service = injector.getInstance(Service.class);

            // call service
            Assertions.assertEquals("Hello 11",service.get(11));

            MethodTrack track = tracker.getLastTrack();
            Assertions.assertTrue(track.toString().contains("get(11) = \"Hello 11\""));
            // object arguments
            Assertions.assertArrayEquals(new Object[] {11},track.getRawArguments());
            // arguments in string form
            Assertions.assertArrayEquals(new String[] {"11"},track.getArguments());
            // raw result
            Assertions.assertEquals("1 call",track.getRawResult());
            // result in string form
            Assertions.assertEquals("1 call",track.getResult());
        });

In this example, trace was enabled (optional) and so each method call would be logged like this:

\\\---[Tracker<Service>] 0.41 ms      <@1b0e9707> .get(11) = "Hello 11"

Configuration

Tracker registration call final Tracker<Service> tracker = hook.track(Service.class) returns a configuration builder. Final registration appears only after .add() method call.

Option Description Default
trace When enabled, all method calls are printed false
slowMethods Print warnings about methods executing longer than the specified threshold. Set to 0 to disable warnings. 5 seconds
disableSlowMethodsLogging Shortcut to disable tracking for slow methods (same as set 0).
keepRawObjects Keep method call arguments and result objects (potentially mutable) true
maxStringLength Max length for a String argument or result (cut long strings) 30

Tracing

Tracing might be useful to see each tracked method call in console with parameters and execution time:

\\\---[Tracker<Service>] 0.41 ms      <@1b0e9707> .foo(1) = "1 call"
\\\---[Tracker<Service>] 0.02 ms      <@1b0e9707> .foo(2) = "2 call"
\\\---[Tracker<Service>] 0.12 ms      <@1b0e9707> .bar(1) = "1 bar"

It also prints service instance hash, to make obvious method calls on different instances. Different instances could appear on prototype-scoped beans (default scope).

Enabled with:

hook.track(Service .class)
        .trace(true)

Note

Traces are logged with System.out to make sure messages are always visible in console.

Slow methods

By default, tracker would log methods, executed longer than 5 seconds:

WARN  [2025-05-09 08:30:38,458] ru.vyarus.dropwizard.guice.test.track.Tracker: 
\\\---[Tracker<Service>] 7.07 ms      <@7634f2b> .foo() = "foo"

Note

Slow methods are logged with logger, and not System.out as traces.

For example, to set slow method for 1 minute:

hook.track(Service .class)
        .slowMethods(1, ChronoUnit.MINUTES)

To avoid logging slow methods (shortcut for setting 0 value):

hook.track(Service .class)
        .disableSlowMethodsLogging()

Keeping raw objects

By default, tracker stores all arguments and returned result objects.

Raw arguments could be used to examine complex objects just after the method call. But, in case of multiple method calls, raw objects might not be actual. For example:

public Service {
    public void foo(List<String> list) {
        list.add("foo" + list.size());
    }
}

Here method changes argument state and so, if we call method multiple times, stored arguments would be useless (as all calls would reference the same list instance):

List<String> test = new ArrayList<>();
service.foo(test);
service.foo(test);

// stored list useless as object was changed after the initial call
List<String> firstCallArg = tracker.getLastTracks(2).get(0).getRawArguments().get(0);
Assertions.assertEquals(2, firstCallArg.size());

// but string representation would still be useful:
String firstCallArgString = tracker.getLastTracks(2).get(0).getArguments().get(0);
Assertions.assertEquals("0[]", firstCallArg.size());

// second call argument string
String firstCallArgString = tracker.getLastTracks(2).get(1).getArguments().get(0);
Assertions.assertEquals("1['foo1']", firstCallArg.size());

In case of complex objects (pojo, for example), string representation would only contain the type and instance hash: Type@hash (which is not informative, but the only universal short way to describe object).

If tracker used only for performance testing (to accumulate execution time from many runs), it might make sense to avoid holding raw arguments:

hook.track(Service .class)
        .keepRawObjects(false)

Max length

Methods could consume or return large string, but using large stings for console output is not desired. All strings larger then configured size would be cut with "..." suffix:

\\\---[Tracker<Service>] 0.08 ms      <@66fb45e5> .baz("largelargelargelargelargelarge...")

Changing default:

hook.track(Service .class)
        .maxStringLength(10)

Tracked data

Each call stored as MethodTrack and contains raw arguments getRawArguments() (which might change over time if mutable objects used) and string version getArguments() (can't change) and same for the result object. Raw objects are mostly useful in case of immediate check after the method call.

Same for result: getRawResult() for raw object and getResult() for string version.

Also, there are quoted string versions: getQuatedResult() and getQuatedArguments(). These methods are the same as string methods, but all strings are in quotes to clearly see string bounds (quoted versions useful for console reporting)

Obtaining tracked data:

// all recordings
List<MethodTrack> tracks = tracker.getTracks();
// last 2 calls (in execution order)
List<MethodTrack> tracks = tracker.getLastTracks(2);
// last call
MethodTrack track = tracker.getLastTrack();

Searching

In the case of many recorded executions (for multiple methods), search could be used:

// search by method (any argument value)
tracks = tracker.findTracks(mock -> when(
               mock.foo(Mockito.anyInt()))
         );

// search methods with argument condition ( > 1) 
tracks = tracker.findTracks(mock -> when(
        mock.foo(Mockito.intThat(argument -> argument > 1)))
        );

// search for methods with exact argument value  
tracks = tracker.findTracks(mock -> when(
        mock.foo(11))
        );

This method uses Mockito stubbing abilities for search criteria declaration: easy to use and type-safe search.

Reset data

Tracked data could be cleared at any time either on tracker: tracker.clear() or using hook (for all trackers): hook.resetTrackers()

Stats

Tracker could aggregate all executions of the same method:

TrackerStats stats = tracker.getStats();
Assertions.assertEquals(1, stats.getMethods().size());

MethodSummary summary = stats.getMethods().get(0);
Assertions.assertEquals("foo", summary.getMethod().getName());
Assertions.assertEquals(Service.class, summary.getService());
Assertions.assertEquals(1, summary.getTracks());
Assertions.assertEquals(0, summary.getErrors());
Assertions.assertEquals(1, summary.getMetrics().getValues().length);
Assertions.assertTrue(summary.getMetrics().getMin() < 1000);

Tracker use dropwizard metrics, so stats provide common values like mean time, median time, 95 percentile, etc.

There is a default statistics report implementation, which might be used for console reporting:

System.out.println(tracker.getStats().render());
    [service]                                [method]                                           [calls]    [fails]    [min]      [max]      [median]   [75%]      [95%]     
    Service                                  foo(int)                                           2 (2)      0          0.009 ms   0.352 ms   0.352 ms   0.352 ms   0.352 ms

Here you can see that 2 instances were used for 2 success calls. Of course max time would be too large (cold jvm), but with min value you can see more realistic time. With a high number of executions percentile and mean values would become more realistic.

Here is an example of tracking GuiceyConfigurationInfo with activated .printAllGuiceBindings() report:

    [service]                                [method]                                           [calls]    [fails]    [min]      [max]      [median]   [75%]      [95%]     
    GuiceyConfigurationInfo                  getNormalModuleIds()                               1          0          1.076 ms   1.076 ms   1.076 ms   1.076 ms   1.076 ms  
    GuiceyConfigurationInfo                  getModulesDisabled()                               1          0          0.038 ms   0.038 ms   0.038 ms   0.038 ms   0.038 ms  
    GuiceyConfigurationInfo                  getOverridingModuleIds()                           1          0          0.034 ms   0.034 ms   0.034 ms   0.034 ms   0.034 ms  
    GuiceyConfigurationInfo                  getExtensionsDisabled()                            1          0          0.020 ms   0.020 ms   0.020 ms   0.020 ms   0.020 ms  
    GuiceyConfigurationInfo                  getOptions()                                       1          0          0.005 ms   0.005 ms   0.005 ms   0.005 ms   0.005 ms  
    GuiceyConfigurationInfo                  getData()                                          3          0          0.003 ms   0.006 ms   0.004 ms   0.006 ms   0.006 ms  

Note

Methods sorted by slowness

You can also collect stats for multiple trackers:

TrackerStats overall = new TrackerStats(tracker1, tracker2);
System.out.println(overall.render());

Tracker object access

If required, existing tracker object could be obtained directly from hook:

Tracker<Service> tracker = hook.getTracker(Service.class);

This might be useful, for example, to obtain multiple trackers and print overall stats. But, in the majority of cases, tracker instance, created on registration would be enough.