3
\$\begingroup\$

I have greatly improved and expanded my my performance testing app

New features include:

  • Builder for type safe creation of a Measurer instances
  • Performs 10 runs of each procedure (default), or any number of runs a user chooses, and then calculates the average for each procedure
  • Provides more comprehensive comparison statistics. Minimum and maximum measurements are also output
  • The averages can be compared against the fastest procedure too (by default, they are still compared against the worst performer). Enum ReferencedProcedure is introduced for convenient customizing
  • measurer.compare() now accepts any number of arguments equal to or greater than two
  • Provides two types of defenses against the "warm-up" issue. A user may choose from the following WarmUpDefender constants: RESTLESS_POINTER (default) and WARM_UP_RUNS_DISCARDER. NONE is also available.

Tweaks:

  • NanoProcessingResult class – gone. nanoProcessor.process() now returns a regular array (I mapped it to an array anyway)
  • ComparisonLoggingIntermediary – gone. What with varargs, it no longer serves the app well

Qualification:

  • I tried to "separate my concerns" and add SLF4J support. The logging doesn't work, though! But let's imagine it does. I know this site is not about fixing code that doesn't work so I don't ask you anything on that. I guess the code itself is fine, but my configuration is faulty (I hate configurations!). If so, it's a client's job to configure it anyway (not my problem if they can't configure their logging)

Client code now may look like this. The example uses anonymous children, but MeasuredProcedureFactory providing some "oven-ready" procedures is still available

        Measurer measurer = Measurer.builder()
                .timesRun(15)
                .warmUpDefender(WarmUpDefender.WARM_UP_RUNS_DISCARDER)
                .compareAgainst(ReferencedProcedure.BEST)
                .build();
        
        measurer.compare(
                new MeasuredProcedure("String.format()") {
                    @Override
                    void run() {
                        var res = String.format("%s%s%s%s%s%s%s%s%s%s", "str1", "str2", "str3", "str4", "str5",
                                "str6", "str7", "str8", "str9", "str10");
                    }
                },
                new MeasuredProcedure("plus concatenation") {
                    @Override
                    void run() {
                        var res = "str1" + "str2" + "str3" + "str4" + "str5" +
                                "str6" + "str7" + "str8" + "str9" + "str10";
                    }
                },
                new MeasuredProcedure("StringBuilder") {
                    @Override
                    void run() {
                        var res = new StringBuilder()
                                .append("str1").append("str2").append("str3")
                                .append("str4").append("str5").append("str6")
                                .append("str7").append("str8").append("str9")
                                .append("str10").toString();
                    }
                }
        );

Here's my new code:

abstract class MeasuredProcedure {
    protected final String name;
    private Collection<Long> measurements;
    protected MeasuredProcedure(String name) {
        this.name = name;
    }
    abstract void run();

    public void setMeasurementsCollection(Collection<Long> measurements) {
        this.measurements = measurements;
    }

    public void addMeasurement(long singleMeasurement) {
        if (measurements == null) {
            measurements = new ArrayList<>();
        }
        measurements.add(singleMeasurement);
    }
    @SuppressWarnings("OptionalGetWithoutIsPresent")
    public long getAverageMeasurement() {
        ensureValidity();
        return (long) measurements.stream()
                .mapToLong(m -> m)
                .average()
                .getAsDouble();
    }
    @SuppressWarnings("OptionalGetWithoutIsPresent")
    public long getMinMeasurement() {
        ensureValidity();
        return measurements.stream()
                .mapToLong(m -> m)
                .min()
                .getAsLong();
    }
    @SuppressWarnings("OptionalGetWithoutIsPresent")
    public long getMaxMeasurement() {
        ensureValidity();
        return measurements.stream()
                .mapToLong(m -> m)
                .max()
                .getAsLong();
    }

    private void ensureValidity() {
        if (measurements == null) {
            throw new IllegalStateException("The measurements Collection is null");
        } else if (measurements.isEmpty()) {
            throw new IllegalStateException("The measurements Collection is empty");
        }
    }

    @Override
    public String toString() {
        return name;
    }
}
@Slf4j
public class Measurer {
    private final int timesRun;
    private final ReferencedProcedure referencedProcedure;
    private final WarmUpDefender warmUpDefender;

    public Measurer() {
        this.timesRun = Defaults.TIMES_RUN;
        this.referencedProcedure = Defaults.REFERENCED_PROCEDURE;
        this.warmUpDefender = Defaults.WARM_UP_DEFENDER;
    }

    private Measurer(int timesRun, ReferencedProcedure referencedProcedure,
                     WarmUpDefender warmUpDefender) {
        this.timesRun = timesRun;
        this.referencedProcedure = referencedProcedure;
        this.warmUpDefender = warmUpDefender;
    }

    public static Builder builder() {
        return new Builder();
    }

    public static class Builder {
        private int timesRun = Defaults.TIMES_RUN;
        private ReferencedProcedure compareAgainst = Defaults.REFERENCED_PROCEDURE;
        private WarmUpDefender warmUpDefender = Defaults.WARM_UP_DEFENDER;

        public Builder timesRun(int timesRun) {
            if (timesRun < 1) {
                throw Defaults.illegalTimesRunException(timesRun);
            }
            this.timesRun = timesRun;
            return this;
        }
        public Builder compareAgainst(ReferencedProcedure compareAgainst) {
            this.compareAgainst = compareAgainst;
            return this;
        }
        public Builder warmUpDefender(WarmUpDefender warmUpDefender) {
            this.warmUpDefender = warmUpDefender;
            return this;
        }
        public Measurer build() {
            return new Measurer(timesRun, compareAgainst,
                    warmUpDefender);
        }
    }

    public long measureAverage(MeasuredProcedure measuredProcedure) {
        measuredProcedure.setMeasurementsCollection(Defaults.measurementsCollection(timesRun));
        warmUpDefender.performMeasurementsWithDefenses(new MeasurementInputs(List.of(measuredProcedure), timesRun));
        log.info(averageMeasurementLog(measuredProcedure));
        return measuredProcedure.getAverageMeasurement();
    }

    private static long measureOnce(MeasuredProcedure measuredProcedure) {
        long start = System.nanoTime();
        measuredProcedure.run();
        long finish = System.nanoTime();
        return finish - start;
    }

    public long compare(MeasuredProcedure firstMeasuredProcedure, MeasuredProcedure secondMeasuredProcedure,
                        MeasuredProcedure... otherMeasuredProcedures) {
        List<MeasuredProcedure> allMeasuredProcedures =
                composeComparedProcedures(firstMeasuredProcedure, secondMeasuredProcedure, otherMeasuredProcedures);
        warmUpDefender.performMeasurementsWithDefenses(new MeasurementInputs(allMeasuredProcedures, timesRun));
        log.info(comparisonStatsLog(allMeasuredProcedures));
        return getSpreadOfAverages(allMeasuredProcedures);
    }

    private List<MeasuredProcedure> composeComparedProcedures(MeasuredProcedure firstMeasuredProcedure,
                                                              MeasuredProcedure secondMeasuredProcedure,
                                                              MeasuredProcedure[] otherMeasuredProcedures) {
        List<MeasuredProcedure> allMeasuredProcedures =
                new ArrayList<>(2 + otherMeasuredProcedures.length);
        allMeasuredProcedures.addAll(List.of(
                firstMeasuredProcedure, secondMeasuredProcedure
        ));
        allMeasuredProcedures.addAll(Arrays.asList(otherMeasuredProcedures));
        return allMeasuredProcedures;
    }

    private long getSpreadOfAverages(List<MeasuredProcedure> allMeasuredProcedures) {
        long max = getMaxAverage(new MeasurementInputs(allMeasuredProcedures, timesRun));
        long min = getMinAverage(new MeasurementInputs(allMeasuredProcedures, timesRun));
        return max - min;
    }

    private static long getMaxAverage(MeasurementInputs inputs) {
        List<MeasuredProcedure> measuredProcedures = inputs.measuredProcedures();
        return measuredProcedures.stream()
                .mapToLong(MeasuredProcedure::getAverageMeasurement)
                .max().orElseThrow(() -> Defaults.illegalTimesRunException(inputs.timesRun()));
    }

    private static long getMinAverage(MeasurementInputs inputs) {
        List<MeasuredProcedure> measuredProcedures = inputs.measuredProcedures();
        return measuredProcedures.stream()
                .mapToLong(MeasuredProcedure::getAverageMeasurement)
                .min().orElseThrow(() -> Defaults.illegalTimesRunException(inputs.timesRun()));
    }

    private String averageMeasurementLog(MeasuredProcedure measuredProcedure) {
        String renderedAverage = renderReadable(measuredProcedure.getAverageMeasurement());
        String onAverageStringAsNecessary = (timesRun == 1) ? "" : "On average,";
        String runSuffixAsNecessary = (timesRun == 1) ? "" : "s";
        return new StringBuilder()
                .append(onAverageStringAsNecessary).append(measuredProcedure)
                .append(" took ").append(renderedAverage)
                .append(" across ").append(timesRun)
                .append(" run").append(runSuffixAsNecessary)
                .toString();
    }

    private String comparisonStatsLog(List<MeasuredProcedure> measuredProcedures) {
        measuredProcedures.sort((p1, p2) -> Math.toIntExact(p1.getAverageMeasurement() - p2.getAverageMeasurement()));
        StringBuilder sb = new StringBuilder();
        appendHeading(sb);
        appendStats(sb, measuredProcedures);
        return sb.toString();
    }

    private void appendHeading(StringBuilder stringBuilder) {
        String tabLeader = Defaults.TAB_LEADER;
        String titlePadding = Defaults.TITLE_PADDING;
        String runSuffixAsNecessary = (timesRun == 1) ? "" : "s";
        stringBuilder
                .append("\n").append(titlePadding).append("Comparison statistics (").append(timesRun)
                .append(" run").append(runSuffixAsNecessary).append(")").append(titlePadding).append("\n\n")
                .append("PROCEDURE").append(tabLeader)
                .append("AVERAGE").append(tabLeader)
                .append("PERCENTAGE OF ").append(referencedProcedure).append(tabLeader)
                .append("MIN").append(tabLeader)
                .append("MAX").append("\n");
    }

    private void appendStats(StringBuilder stringBuilder, List<MeasuredProcedure> measuredProcedures) {
        String tabLeader = Defaults.TAB_LEADER;
        long referencedAverage = referencedProcedure.getReference(new MeasurementInputs(measuredProcedures, timesRun));
        MeasuredProcedure currentProcedure;
        long bestResult = measuredProcedures.get(0).getAverageMeasurement();
        long currentAverage, averageAsPercentageOfReference;
        String renderedCurrentAverage, renderedMarginToBestPerformerAsNecessary, renderedMin, renderedMax;
        for (int i = 0; i < measuredProcedures.size(); i++) {
            currentProcedure = measuredProcedures.get(i);
            currentAverage = currentProcedure.getAverageMeasurement();
            renderedCurrentAverage = renderReadable(currentAverage);
            averageAsPercentageOfReference = (long) ((double) currentAverage / referencedAverage * 100);
            renderedMarginToBestPerformerAsNecessary = (i == 0) ? "" :
                    " (+ " + renderReadable(currentAverage - bestResult) + ")";
            renderedMin = renderReadable(currentProcedure.getMinMeasurement());
            renderedMax = renderReadable(currentProcedure.getMaxMeasurement());
            stringBuilder.append(i + 1).append(". ").append(currentProcedure).append(tabLeader)
                    .append(renderedCurrentAverage).append(renderedMarginToBestPerformerAsNecessary).append(tabLeader)
                    .append(averageAsPercentageOfReference).append("%").append(tabLeader)
                    .append(renderedMin).append(tabLeader)
                    .append(renderedMax).append("\n");
        }
    }


    private static String renderReadable(long nanos) {
        TimeUnit[] timeUnits = new NanoProcessor().process(nanos);
        var joiner = new StringJoiner(", ");
        for (TimeUnit unit : timeUnits) {
            if (unit.value() == 0) {
                continue;
            }
            String unitSuffixAsNecessary = (unit.value() % 10 == 1) ? "" : "s";
            joiner.add(unit.value() + " " + unit.name() + unitSuffixAsNecessary);
        }
        return joiner.toString();
    }

    private static class Defaults {
        static final int TIMES_RUN = 10;
        static final ReferencedProcedure REFERENCED_PROCEDURE = ReferencedProcedure.WORST;
        static final WarmUpDefender WARM_UP_DEFENDER = WarmUpDefender.RESTLESS_POINTER;
        static final int NUM_OF_WARM_UP_RUNS = 2;
        static final String TAB_LEADER = " " + "-".repeat(10) + " ";
        static final String TITLE_PADDING = ":".repeat(20);
        static RuntimeException illegalTimesRunException(int illegalTimesRun) {
            return new IllegalArgumentException
                    ("A procedure cannot be run less than one time. Your value: " + illegalTimesRun);
        }
        static Collection<Long> measurementsCollection(int initialCapacity) {
            return new ArrayList<>(initialCapacity);
        }
    }

    public enum WarmUpDefender {
        RESTLESS_POINTER {
            void performMeasurementsWithDefenses(MeasurementInputs inputs) {
                List<MeasuredProcedure> allMeasuredProcedures = inputs.measuredProcedures();
                MeasuredProcedure currentMeasuredProcedure;
                int procedurePointer = 0;
                for (int i = 0; i < inputs.timesRun() * allMeasuredProcedures.size(); i++) {
                    if (procedurePointer == allMeasuredProcedures.size()) {
                        procedurePointer = 0;
                    }
                    currentMeasuredProcedure = allMeasuredProcedures.get(procedurePointer);
                    long singleMeasurement = measureOnce(currentMeasuredProcedure);
                    currentMeasuredProcedure.addMeasurement(singleMeasurement);
                    procedurePointer++;
                }
            }
        }, WARM_UP_RUNS_DISCARDER {
            void performMeasurementsWithDefenses(MeasurementInputs inputs) {
                List<MeasuredProcedure> allMeasuredProcedures = inputs.measuredProcedures();
                warmUp(allMeasuredProcedures);
                for (MeasuredProcedure measuredProcedure : allMeasuredProcedures) {
                    for (int i = 0; i < inputs.timesRun(); i++) {
                        long singleMeasurement = measureOnce(measuredProcedure);
                        measuredProcedure.addMeasurement(singleMeasurement);
                    }
                }
            }
            private void warmUp(List<MeasuredProcedure> allMeasuredProcedures) {
                for (int i = 0; i < numOfWarmUpRuns; i++) {
                    for (MeasuredProcedure measuredProcedure: allMeasuredProcedures) {
                        measureOnce(measuredProcedure);
                    }
                }
            }
        }, NONE {
            void performMeasurementsWithDefenses(MeasurementInputs inputs) {
                List<MeasuredProcedure> allMeasuredProcedures = inputs.measuredProcedures();
                MeasuredProcedure currentMeasuredProcedure;
                for (MeasuredProcedure allMeasuredProcedure : allMeasuredProcedures) {
                    currentMeasuredProcedure = allMeasuredProcedure;
                    for (int ii = 0; ii < inputs.timesRun(); ii++) {
                        long singleMeasurement = measureOnce(currentMeasuredProcedure);
                        currentMeasuredProcedure.addMeasurement(singleMeasurement);
                    }
                }

            }
        };
        private static final int numOfWarmUpRuns = Defaults.NUM_OF_WARM_UP_RUNS;
        abstract void performMeasurementsWithDefenses(MeasurementInputs inputs);
    }

    public enum ReferencedProcedure {
        BEST {
            long getReference(MeasurementInputs inputs) {
                return getMinAverage(inputs);
            }
        }, WORST {
            long getReference(MeasurementInputs inputs) {
                return getMaxAverage(inputs);
            }
        };
        abstract long getReference(MeasurementInputs inputs);
    }

    private record MeasurementInputs(List<MeasuredProcedure> measuredProcedures, int timesRun) {}

    static class NanoProcessor {
        TimeUnit[] process(long nanos) {
            long minutes = extractLooseMinutes(nanos);
            long seconds = extractLooseSeconds(nanos);
            long milliseconds = extractLooseMilliseconds(nanos);
            long nanoseconds = extractLooseNanoseconds(nanos);
            return new TimeUnit[]{
                    new TimeUnit(TimeUnitName.MINUTE, minutes),
                    new TimeUnit(TimeUnitName.SECOND, seconds),
                    new TimeUnit(TimeUnitName.MILLISECOND, milliseconds),
                    new TimeUnit(TimeUnitName.NANOSECOND, nanoseconds)
            };
        }

        private long extractLooseMinutes(long nanos) {
            return nanos / 60_000_000_000L;
        }

        private long extractLooseSeconds(long nanos) {
            return nanos % 60_000_000_000L / 1_000_000_000L;
        }

        private long extractLooseMilliseconds(long nanos) {
            return nanos % 1_000_000_000L / 1_000_000L;
        }

        private long extractLooseNanoseconds(long nanos) {
            return nanos % 1_000_000L;
        }

        record TimeUnit(TimeUnitName name, long value) {
            enum TimeUnitName {
                MINUTE, SECOND, MILLISECOND, NANOSECOND;

                @Override
                public String toString() {
                    return name().toLowerCase();
                }
            }
        }
    }
}

Some questions I have in mind:

  1. I put the defaults in a nested static "container" class called Defaults. Is it good or bad design?
  2. I removed a couple of unnecessary classes, but added another, MeasurementInputs. Bob Martin said the less you pass, the better so I figured I'll pass some container object instead of two parameters. (and I also read somewhere that if you pass the same collection of arguments over and over again, it may be the sign that you should encapsulate it) At any rate, you have to admit, it's awkward. Besides, it's mainly because I use the non-static timesRun field. What do you recommend?
  3. Can I make my comparison output prettier? I guess I should make tab leaders dynamic in length somehow, is that correct?
  4. The number of "warm-up" runs is hard-coded. It's a shame enum constants can't have their own fields and methods, not shared by other constants. I could do something like this
    public enum WarmUpDefender {
        WARM_UP_RUNS_DISCARDER {
            private int numOfWarmUpRuns = Defaults.WARM_UP_DEFENDER;
            public WarmUpDefender withNumOfWarmUpRuns(int numOfWarmUpRuns) {
                this.numOfWarmUpRuns = numOfWarmUpRuns;
                return this;
            }
.warmUpDefender(WarmUpDefender.WARM_UP_RUNS_DISCARDER.withNumOfWarmUpRuns(10)) // doesn't work ((

What do you suggest instead?

  1. If you run the test I pasted above, you get output that is kind of weird
::::::::::::::::::::Comparison statistics (15 runs)::::::::::::::::::::

PROCEDURE ---------- AVERAGE ---------- PERCENTAGE OF BEST ---------- MIN ---------- MAX
1. plus concatenation ---------- 153 nanoseconds ---------- 100% ---------- 100 nanoseconds ---------- 300 nanoseconds
2. StringBuilder ---------- 726 nanoseconds (+573 nanoseconds) ---------- 474% ---------- 600 nanoseconds ---------- 1500 nanoseconds
3. String.format() ---------- 41573 nanoseconds (+41420 nanoseconds) ---------- 27171% ---------- 15900 nanoseconds ---------- 81100 nanoseconds

It can't be true, can it? Plus concatenation must be the slowest, mustn't it? Which brings the question: did the JVM just "optimize my code into nothingness"? If so, why didn't it optimize into nothingness the String.format() code?

You may also need some way to safely "sink" results in way that guarantees that a result will be computed, to prevent the JVM from optimizing code into nothingness when it detects that the result is not used.

–Harold, the original post

Or was it something else?

  1. What about all those "asNecessary" strings? Is there a better way to handle that?

UPD: As I thought, the logging issue had to do with my configuration, not the code. I fixed it adding this dependency, for those interested (thanks, ChatGPT!)

    <dependency>
      <groupId>com.fasterxml.jackson.dataformat</groupId>
      <artifactId>jackson-dataformat-yaml</artifactId>
      <version>2.15.2</version>
    </dependency>
\$\endgroup\$
2
  • 1
    \$\begingroup\$ I think this entire codebase is over-engineered. I think it's much more complex than it needs to be for what is fundamentally a simple set of operations. For a more straightforward approach, see codereview.stackexchange.com/questions/82439/…. I also think you ignored some good advice in prior reviews. You might consider rereading the suggestions there and taking them to heart. \$\endgroup\$
    – Eric Stein
    Jun 19, 2023 at 18:40
  • \$\begingroup\$ @EricStein I think I did follow the advice... 🤔 \$\endgroup\$
    – Sergey
    Jun 19, 2023 at 19:21

2 Answers 2

2
\$\begingroup\$

Questions, as far as I chose to answer them

I put the defaults in a nested static "container" class called Defaults. Is it good or bad design?

I don't mind it, but I question why there are also "utility functions" on it.

Can I make my comparison output prettier? I guess I should make tab leaders dynamic in length somehow, is that correct?

There are some libraries to print text-mode tables, which can handle flexible-width columns. Fixed-width columns are easy but wouldn't deal gracefully with the variable-length benchmark names. Well, it's not as if flexible-width columns are difficult either.

The number of "warm-up" runs is hard-coded. It's a shame enum constants can't have their own fields and methods, not shared by other constants. .warmUpDefender(WarmUpDefender.WARM_UP_RUNS_DISCARDER.withNumOfWarmUpRuns(10)) // doesn't work What do you suggest instead?

Instead of an enum, you could use a class with several static factory functions. Then you could write:

WarmUpDefender.warmUpRunsDiscarder().withNumOfWarmUpRuns(10)

It can't be true, can it? Plus concatenation must be the slowest, mustn't it? Which brings the question: did the JVM just "optimize my code into nothingness"? If so, why didn't it optimize into nothingness the String.format() code?

There's a simpler issue.

The code was this:

var res = "str1" + "str2" + "str3" + "str4" + "str5" +
          "str6" + "str7" + "str8" + "str9" + "str10";

Given that the strings being "plussed together" (this strange terminology is just to avoid all doubt about how the strings are concatenated, which may matter for all I know, at least there is no a-priori reason to assume that all ways to concatenate strings are equivalent) are literal strings, there's nothing standing in the way of the java compiler itself (not even the JVM) doing the concatenation, which indeed is what happens. Note that there are various other (more powerful) string concatenation optimizations done by various different versions of the java compiler. That may get in the way of what you intended to measure, but that depends on what your intentions are, it's not inherently wrong to measure the performance of the above code, and then discover that something interesting happened.

Anyway, there is no actual concatenation happening for you to measure there, only at most loading a string constant from the constant pool and then immediately discarding it. That, in turn, may be optimized out by the JVM - which I did not confirm because getting an assembly listing of JIT compiled methods takes some strange set-up that I haven't done. If you wanted to measure the performance of an actual concatenation actually happening, you'll have to do more effort to guarantee that it actually happens (this "fighting the compiler to get it to actually produce the code that we wanted to benchmark" nonsense is the main reason why I prefer benchmarking assembly code instead of higher level code, but of course in this context you don't have that luxury). This specific thing isn't the frameworks fault, it couldn't have done anything to prevent it.

In general you probably still need a way to safely ignore results to prevent unwanted optimizations, though that alone would not fix this particular problem.


Warm-up

There is some warm-up protection now, but I think it's very little. I would not be confident that the right thing is being measured, nor that the CPU has come up to speed yet.

Danger zone

"Timing" code works well, and has a reasonable interpretation, for macroscopic chunks of code. At some point when scaling the code down, there comes a point where it no longer makes sense to talk about code "taking time" - latency and throughput will separate and go each their own way (with latency acting mostly the way you'd expect, but throughput becomes non-additive: to get the combined throughput of two snippets, you would have to reason about µops and execution ports, not their individual throughputs), and it becomes very important exactly how the code is structured, not just what's in it.

"plus concatenation", since it accidentally ended up doing essentially nothing (and doesn't have a well-defined latency), is far into this danger zone where code does not "take time" - it has a cost, and that cost eventually manifests in taking time, but that cost is not itself expressible as time. That does not mean that it's wrong to try to time it, you still get something that you can draw conclusions from (though the conclusion this time is probably "we accidentally benchmarked the wrong thing due to compiler optimizations"), but paradoxically you cannot get "the time it takes" from it.

\$\endgroup\$
4
  • \$\begingroup\$ Thank you! Can you expand on this: "There are some libraries to print text-mode tables, which can handle flexible-width columns."? ChatGPT suggested this: TextTable table = new TextTable(new String[]{"PROCEDURE", "AVERAGE", "PERCENTAGE OF BEST", "MIN", "MAX"}); table.addRow("1. plus concatenation", "153 nanoseconds", "100%", "100 nanoseconds", "300 nanoseconds"); But unfortunately, it completely made all this up. Apache Commons Text doesn't have a TextTable class that has an addRow() method \$\endgroup\$
    – Sergey
    Jun 22, 2023 at 6:00
  • \$\begingroup\$ @Sergey classic ChatGPT, plausible nonsense half the time.. anyway j-text-utils has a TextTable, but not with addRow (you feed it all of the data in its constructor), and Java Text Tables can probably do it (also not with addRow, and it's not called TextTable). There is an AsciiTable with addRow method. \$\endgroup\$
    – harold
    Jun 22, 2023 at 6:28
  • \$\begingroup\$ This "Ascii art" can't possibly be responsive, can it? I dabbled with some library, and it looks good only if your window is wide enough (screenshot). Also, did you mean this ASCII Table? \$\endgroup\$
    – Sergey
    Jun 22, 2023 at 7:57
  • 1
    \$\begingroup\$ I meant this one, and I don't expect any good solutions to the "narrow window problem" - a table printer is not a GUI, not even one of those fake text-mode GUIs, you won't get a horizontal scroll bar \$\endgroup\$
    – harold
    Jun 22, 2023 at 8:09
2
\$\begingroup\$

Some thoughts after a glimpse.

  1. I put the defaults in a nested static "container" class called Defaults. Is it good or bad design?

From a domain specific language (DSL) stand point is an impediment. From single responsibility principle (SRP) is arguable Defaults responsibility is to provide default values and the flip side is "should there be the distinct responsibility of providing default values?".

  1. I removed a couple of unnecessary classes, but added another, MeasurementInputs. Bob Martin said the less you pass, the better so I figured I'll pass some container object instead of two parameters. (and I also read somewhere that if you pass the same collection of arguments over and over again, it may be the sign that you should encapsulate it) At any rate, you have to admit, it's awkward. Besides, it's mainly because I use the non-static timesRun field. What do you recommend?

Before this question I wondered what's the purpose of the whole implementation? There are already available profilers, why not use one already available.

  1. Can I make my comparison output prettier? I guess I should make tab leaders dynamic in length somehow, is that correct?

A first improvement would be to factor out the output methods in a separate class.

  1. The number of "warm-up" runs is hard-coded. It's a shame enum constants can't have their own fields and methods, not shared by other constants.

The enum is called WarmUpDefender there wouldn't be any confusion if it would provide the constant NUM_OF_WARM_UP_RUNS otherwise for the use case of passing different default for different runs add the default to MeasurementInputs.

  1. did the JVM just "optimize my code into nothingness"? If so, why didn't it optimize into nothingness the String.format() code?

It might have been the Java-In-Timer compiler (JIT).

  1. What about all those "asNecessary" strings? Is there a better way to handle that?

An intermediary step would be the one given by the answer to the 3rd question.

  1. not asked question

ensureValidity(); could be replaced by wrapping the measurements in an Optional filtered, mapped and orElseThrow an exception from a Supplier previously initialised and stored in a private static final variable.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.