Inspired by ChucK, and also by the shape of traffic patterns I recently encountered in one of my projects (which looked like reverse saw waves), waveshaper uses musical (or otherwise synthetic) waveforms for load testing applications. This is made possible through the implementation of a signal oscillator that generates a number of waveforms (see below), which is then chained to a guava RateLimiter. This threadsafe rate limiter can then be utilized by your data generation / load testing tools, as seen in the demo below.
The following waveforms are currently supported
Waveform | Class | Shape |
---|---|---|
Sine | SineWave.java | ▁▂▄▆██▆▄▂▁▁▂▄▆██▆▄▂▁▁▂▄▆██▆▄▂ |
Saw | SawWave.java | ▁▁▂▃▄▄▅▆▇█▁▁▂▃▄▄▅▆▇█▁▁▂▃▄▄▅▆▇█▁▁ |
Reverse saw | ReverseSawWave.java | █▇▆▅▄▄▃▂▁▁█▇▆▅▄▄▃▂▁▁█▇▆▅▄▄▃▂▁ |
Square | SquareWave.java | ▁▁█████▁▁▁▁▁█████▁▁▁▁▁█████▁▁ |
Triangle | Triangle.java | ▁▂▃▅▆█▆▅▃▂▁▂▃▅▆█▆▅▃▂▁▂▃▅▆█▆▅▃▂▁ |
Gradle:
implementation 'io.waveshaper:waveshaper:0.1.3'
Maven:
<dependency>
<groupId>io.waveshaper</groupId>
<artifactId>waveshaper</artifactId>
<version>0.1.3</version>
</dependency>
Jar downloads are available from Maven Central.
The repo includes a demo. You can run it using the following commands:
export WAVE="Sine" # Saw, Square, ReverseSaw, Triangle
./gradlew runDemo1
// create an oscillator that generates the following waveform:
// ▁▁▂▃▄▄▅▆▇█▁▁▂▃▄▄▅▆▇█▁▁▂▃▄▄▅▆▇█▁▁
Oscillator osc =
new Oscillator.Builder()
.waveform(SawWave::new)
.cycles(3)
.sampleRate(8)
.sampleDuration(Duration.ofSeconds(5))
.range(1, 500_000)
.build();
// synchronize the oscillator signal with a rate limiter (signal chaining)
WaveformRateLimiter rateLimiter = WaveformRateLimiter.create(osc);
// generate a threadpool to execute our tasks
ExecutorService executor = Executors.newFixedThreadPool(10, new ProducerThreadFactory());
// track the amount of permits that are issued to each thread
AtomicLongMap<String> permitsByThread = AtomicLongMap.create();
while (rateLimiter.updating()) {
// try to acquire a permit for doing work. this will block if we're being
// rate limited
rateLimiter.acquire();
// yay, we're unblocked! submit a task to our threadpool
executor.execute(
() -> {
// increment the permit count for the executing thread
permitsByThread.incrementAndGet(Thread.currentThread().getName());
// do work here. e.g. if you are producing a message to kafka, this is
// where you'd do it
});
}
In addition to synthetic waveforms, waveshaper also allows you to build data generators for your load testing activities using a navigable, InfiniteSequence. These infinite sequences can generate any type of data. Here is an example:
InfiniteSequence<String> seq =
new InfiniteSequence.Builder<String>()
.add("hello")
.add("world")
.build();
while (true) {
System.out.println(seq.next());
}
produces the following output (which would continue forever):
hello
world
hello
world
hello
world
Dynamic, infinite sequences are also possible. Simply provide a callable that accepts a context and navigator object, and utilize the context to either get info about the current iteration (good for dynamic ID generation) or to set custom data for other steps to utilize.
BiFunction<Context, Navigator, String> sayHello =
(ctx, nav) -> {
// generate some fields to be used by a later step.
// ctx.iteration() is convenient for id generation
ctx.set("id", "abc" + ctx.iteration());
ctx.set("name", "mitch");
return "hello, mitch";
};
BiFunction<Context, Navigator, String> sayGoodbye =
(ctx, nav) -> {
// use a field that was set by a previous step in this sequence
return "goodbye, " + ctx.get("name");
};
InfiniteSequence<String> seq =
new InfiniteSequence.Builder<String>()
.add(sayHello)
.add(sayGoodbye)
.build();
while (true) {
System.out.println(seq.next());
}
The above would produce the following:
hello, mitch
goodbye, mitch
hello, mitch
goodbye, mitch
...
Finally, you can use the navigator object to move through an infinite sequence non-linearly.
BiFunction<Context, Navigator, String> sometimesRequeue =
(ctx, nav) -> {
if (ctx.iteration() % 2 == 0) {
nav.back(1);
}
return "queued";
};
InfiniteSequence<String> seq =
new InfiniteSequence.Builder<String>()
.add("pending")
.add(sometimesRequeue)
.add("delivered")
.build();