All Downloads are FREE. Search and download functionalities are using the official Maven repository.

eu.fbk.knowledgestore.tool.TestDriver Maven / Gradle / Ivy

Go to download

A collection of command line tools for interacting with a KnowledgeStore server, including benchmarking tools to create and perform a performance test of KS retrieval methods as well as a tool for dumping the contents of a KS instance to RDF files.

There is a newer version: 1.7.1
Show newest version
package eu.fbk.knowledgestore.tool;

import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.Writer;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Random;
import java.util.Set;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.atomic.AtomicReference;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import javax.annotation.Nullable;

import com.google.common.base.Charsets;
import com.google.common.base.Joiner;
import com.google.common.base.Preconditions;
import com.google.common.base.Splitter;
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Ordering;
import com.google.common.collect.Sets;
import com.google.common.primitives.Ints;
import com.google.common.primitives.Longs;
import com.google.common.util.concurrent.ThreadFactoryBuilder;

import org.apache.commons.math3.stat.descriptive.DescriptiveStatistics;
import org.openrdf.model.URI;
import org.openrdf.model.Value;
import org.openrdf.model.ValueFactory;
import org.openrdf.model.impl.ValueFactoryImpl;
import org.openrdf.query.Binding;
import org.openrdf.query.BindingSet;
import org.openrdf.query.impl.MapBindingSet;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.slf4j.MDC;

import eu.fbk.knowledgestore.Operation.Sparql;
import eu.fbk.knowledgestore.Session;
import eu.fbk.knowledgestore.client.Client;
import eu.fbk.knowledgestore.data.Data;
import eu.fbk.knowledgestore.data.Record;
import eu.fbk.knowledgestore.data.Representation;
import eu.fbk.knowledgestore.data.Stream;
import eu.fbk.knowledgestore.internal.CommandLine;
import eu.fbk.knowledgestore.internal.Logging;
import eu.fbk.knowledgestore.vocabulary.KS;
import eu.fbk.rdfpro.util.IO;
import eu.fbk.rdfpro.util.Namespaces;
import eu.fbk.rdfpro.util.Statements;
import eu.fbk.rdfpro.util.Tracker;

public final class TestDriver {

    private static final Logger LOGGER = LoggerFactory.getLogger(TestDriver.class);

    private static final ValueFactory FACTORY = ValueFactoryImpl.getInstance();

    private final String url; // test.url

    private final String username; // test.username

    private final String password; // test.password

    private final int warmupMixes; // test.warmupmixes

    private final int testMixes; // test.testmixes

    private final long warmupTime; // test.warmuptime

    private final long testTime; // test.testtime

    private final int clients; // test.clients

    private final long timeout; // test.timeout

    private final Query[] queries; // test.queries

    private final List outputVariables;

    private List inputVariables;

    private final byte[][] inputData;

    private final File outputFile;

    private final long seed;

    public static void main(final String... args) {
        try {
            // MDC.put(Logging.MDC_CONTEXT, "main");

            final CommandLine cmd = CommandLine
                    .parser()
                    .withName("ks-test-driver")
                    .withHeader(
                            "Perform a query scalability test against a KnowledgeStore. "
                                    + "Test parameters and queries are supplied in a .properties file. "
                                    + "Test data (produced with query-test-generator) "
                                    + "is supplied in a .tsv file.")
                    .withOption("c", "config", "the configuration file", "FILE",
                            CommandLine.Type.FILE_EXISTING, true, false, true)
                    .withFooter(
                            "Test configuration may be overridden by supplying additional "
                                    + "property=value\narguments on the command line.")
                    .withLogger(LoggerFactory.getLogger("eu.fbk.nwrtools")).parse(args);

            final File configFile = cmd.getOptionValue("c", File.class);

            final Properties config = new Properties();
            try (InputStream configStream = IO.read(configFile.getAbsolutePath())) {
                config.load(configStream);
            }

            for (final String arg : cmd.getArgs(String.class)) {
                final int index = arg.indexOf('=');
                if (index > 0) {
                    final String name = arg.substring(0, index);
                    final String value = arg.substring(index + 1);
                    config.setProperty(name, value);
                }
            }

            new TestDriver(config, configFile.getParentFile()).run();

        } catch (final Throwable ex) {
            CommandLine.fail(ex);
        }
    }

    public TestDriver(final Properties properties, @Nullable final File baseDir)
            throws IOException {

        // Get base path
        final Path base = (baseDir != null ? baseDir : new File(System.getProperty("user.dir")))
                .toPath();

        // Parse seed
        this.seed = TestUtil.read(properties, "test.seed", Long.class, 0L);

        // Parse file names
        final String dataArg = TestUtil.read(properties, "test.data", String.class);
        final String outputArg = TestUtil.read(properties, "test.out", String.class);
        final File dataFile = base.resolve(Paths.get(dataArg)).toFile();
        this.outputFile = outputArg == null ? null : base.resolve(Paths.get(outputArg)).toFile();

        // Parse server URL, username and password
        this.url = TestUtil.read(properties, "test.url", String.class);
        this.username = TestUtil.read(properties, "test.username", String.class, null);
        this.password = TestUtil.read(properties, "test.password", String.class, null);
        LOGGER.info("SUT: {}{}", this.url,
                this.username == null && this.password == null ? " (anonymous access)"
                        : " (authenticated access)");

        // Parse number of mixes, max times and client counts
        this.warmupMixes = TestUtil.read(properties, "test.warmupmixes", Integer.class, 0);
        this.testMixes = TestUtil.read(properties, "test.testmixes", Integer.class, 1);
        this.warmupTime = TestUtil.read(properties, "test.warmuptime", Long.class, 3600L) * 1000;
        this.testTime = TestUtil.read(properties, "test.testtime", Long.class, 3600L) * 1000;
        this.clients = TestUtil.read(properties, "test.clients", Integer.class, 1);
        LOGGER.info("{} mix(es), {} s warmup; {} mix(es), {} s test; {} client(s)",
                this.warmupMixes, this.warmupTime / 1000, this.testMixes, this.testTime / 1000,
                this.clients);

        // Parse default timeout
        this.timeout = TestUtil.read(properties, "test.timeout", Long.class, -1L);

        // Parse test data
        Preconditions.checkArgument(dataFile.exists(), "File " + dataFile + " does not exist");
        final List data = Lists.newArrayList();
        try (BufferedReader reader = new BufferedReader(IO.utf8Reader(IO.buffer(IO.read(dataFile
                .getAbsolutePath()))))) {
            String line = reader.readLine();
            final String[] inputVariables = line.split("\t");
            for (int i = 0; i < inputVariables.length; ++i) {
                inputVariables[i] = inputVariables[i].substring(1);
            }
            this.inputVariables = ImmutableList.copyOf(inputVariables);
            LOGGER.info("Input schema: ({})", Joiner.on(", ").join(this.inputVariables));
            final Tracker tracker = new Tracker(LOGGER, null, //
                    "Parsed " + dataFile + ": %d tuples (%d tuple/s avg)", //
                    "Parsed %d tuples (%d tuple/s, %d tuple/s avg)");
            tracker.start();
            while ((line = reader.readLine()) != null) {
                data.add(line.getBytes(Charsets.UTF_8));
                tracker.increment();
            }
            tracker.end();
        }
        this.inputData = data.toArray(new byte[data.size()][]);

        // Parse queries
        final Properties defaultQueryProperties = new Properties();
        if (this.timeout >= 0) {
            defaultQueryProperties.setProperty("timeout", Long.toString(this.timeout));
        }
        final List allQueries = Query.create(properties, defaultQueryProperties);
        final List enabledQueries = Lists.newArrayList();
        final Set enabledNames = Sets.newLinkedHashSet(Arrays.asList(TestUtil.read(
                properties, "test.queries", String.class).split("\\s*[,]\\s*")));
        for (final String name : enabledNames) {
            boolean added = false;
            for (final Query query : allQueries) {
                if (query.getName().equals(name)) {
                    enabledQueries.add(query);
                    added = true;
                    break;
                }
            }
            Preconditions.checkArgument(added, "Unknown query " + name);
        }
        this.queries = enabledQueries.toArray(new Query[enabledQueries.size()]);
        LOGGER.info("{} queries enabled ({} defined): {}", enabledQueries.size(),
                allQueries.size(), Joiner.on(", ").join(enabledQueries));

        // Check query variables and build list of output variables
        final List outputVariables = Lists.newArrayList("mix.client", "mix.index",
                "mix.input", "mix.start", "mix.time");
        for (final String variable : this.inputVariables) {
            outputVariables.add("input." + variable);
        }
        for (final Query query : this.queries) {
            for (final String inputVariable : query.getInputVariables()) {
                if (!this.inputVariables.contains(inputVariable)) {
                    throw new IllegalArgumentException("Query " + query
                            + " refers to unknown input variable " + inputVariable);
                }
            }
            for (final String outputVariable : Ordering.natural().immutableSortedCopy(
                    query.getOutputVariables())) {
                outputVariables.add(query.getName() + "." + outputVariable);
            }
        }
        this.outputVariables = ImmutableList.copyOf(outputVariables);
        LOGGER.info("Output schema: {} attributes", this.outputVariables.size());
    }

    public void run() throws Throwable {

        // Open writer if possible
        final Writer writer = this.outputFile == null ? null : IO.utf8Writer(IO.buffer(IO
                .write(this.outputFile.getAbsolutePath())));

        try {
            // Allocate a random number generator
            final Random random = new Random(this.seed);

            // Take a timestamp and allocate data structure for computing statistics
            final long ts = System.currentTimeMillis();
            final List queryNames = Lists.newArrayList();
            for (final Query query : this.queries) {
                queryNames.add(query.getName());
            }
            final Statistics stats = new Statistics(queryNames);

            // Log test started
            LOGGER.info("Test started");

            // Perform warmup (if enabled)
            if (this.warmupMixes > 0) {
                runClients(this.warmupMixes, this.warmupTime, random, "Warmup", null, null);
            }

            // Perform the real test (if enabled)
            if (this.clients > 0 && this.testMixes > 0) {
                if (writer != null) {
                    for (int i = 0; i < this.outputVariables.size(); ++i) {
                        writer.write(i == 0 ? "?" : "\t?");
                        writer.write(this.outputVariables.get(i));
                    }
                    writer.write("\n");
                }
                runClients(this.testMixes, this.testTime, random, "Measurement", writer, stats);
            }

            // Log test completion
            LOGGER.info("Test completed in {} ms\n\n{}\n", System.currentTimeMillis() - ts, stats);

        } finally {
            // Close TSV file
            IO.closeQuietly(writer);
        }
    }

    private void runClients(final int maxMixes, final long maxTime, final Random random,
            final String phaseName, @Nullable final Writer writer, @Nullable final Statistics stats)
            throws Throwable {

        // Log start
        LOGGER.info("{} started ({} clients, {} mix(es), {} queries/mix)", phaseName,
                this.clients, maxMixes, this.queries.length);

        // Create a Tracker to track the progress of the process
        final Tracker tracker = new Tracker(LOGGER, null,
                "Completed %d query mixes (%d mixes/s avg)",
                "Completed %d query mixes (%d mixes/s, %d mixes/s avg)");
        tracker.start();

        // Start a thread for each concurrent client
        final AtomicReference exceptionHolder = new AtomicReference<>();
        final Thread mainThread = Thread.currentThread();
        final ExecutorService executor = Executors.newCachedThreadPool(new ThreadFactoryBuilder()
                .setDaemon(true).setNameFormat("client-%02d").build());
        final AtomicLong startTimestamp = new AtomicLong(Long.MAX_VALUE);
        final AtomicLong endTimestamp = new AtomicLong(Long.MIN_VALUE);
        final long[] clientExecutionTimes = new long[this.clients];
        final int[] clientMixes = new int[this.clients];
        try {
            final AtomicInteger globalMixCounter = new AtomicInteger(maxMixes);
            for (int i = 0; i < this.clients; ++i) {
                final int clientId = i;
                executor.submit(new Runnable() {

                    @Override
                    public void run() {
                        final String oldContext = MDC.get(Logging.MDC_CONTEXT);
                        try {
                            MDC.put(Logging.MDC_CONTEXT, String.format("client%d", clientId));
                            final AtomicInteger localMixCounter = new AtomicInteger(0);
                            final long startTs = System.currentTimeMillis();
                            synchronized (startTimestamp) {
                                if (startTs < startTimestamp.get()) {
                                    startTimestamp.set(startTs);
                                }
                            }
                            final long endTs = runClient(clientId, globalMixCounter,
                                    localMixCounter, maxTime, random, tracker, startTs, writer,
                                    stats);
                            clientExecutionTimes[clientId] = endTs - startTs;
                            clientMixes[clientId] = localMixCounter.get();
                            synchronized (endTimestamp) {
                                if (endTs > endTimestamp.get()) {
                                    endTimestamp.set(endTs);
                                }
                            }
                        } catch (final Throwable ex) {
                            LOGGER.error("[{}] Client failed", clientId, ex);
                            exceptionHolder.compareAndSet(null, ex);
                            mainThread.interrupt(); // Stop waiting for other threads
                        } finally {
                            MDC.put(Logging.MDC_CONTEXT, oldContext);
                        }
                    }

                });
            }
            executor.shutdown();
            executor.awaitTermination(Integer.MAX_VALUE, TimeUnit.MILLISECONDS);

        } catch (final InterruptedException ex) {
            final Throwable ex2 = exceptionHolder.get();
            if (ex2 != null) {
                ex.addSuppressed(ex2); // keep track of both exceptions
            }
            throw ex;

        } finally {
            executor.shutdownNow();
            tracker.end();
        }

        // Report exception, if any
        if (exceptionHolder.get() != null) {
            throw exceptionHolder.get();
        }

        // Compute and report elapsed time
        final long elapsed = endTimestamp.get() - startTimestamp.get();
        if (stats != null) {
            stats.reportElapsedTest(elapsed);
        }

        // Log completion
        LOGGER.info("{} completed in {} ms (client time: {}-{} ms; client mixes: {}-{})",
                phaseName, elapsed, Longs.min(clientExecutionTimes),
                Longs.max(clientExecutionTimes), Ints.min(clientMixes), Ints.max(clientMixes));
    }

    private long runClient(final int clientId, final AtomicInteger globalMixCounter,
            final AtomicInteger localMixCounter, final long maxTime, final Random random,
            @Nullable final Tracker tracker, final long startTimestamp,
            @Nullable final Writer writer, @Nullable final Statistics stats) throws IOException {

        // Log start
        LOGGER.debug("Client started");

        // Initialize a client and open a session with the SUT
        long timestamp = startTimestamp;
        try (Client client = Client.builder(this.url).compressionEnabled(true)
                .validateServer(false).build()) {
            try (final Session session = client.newSession(this.username, this.password)) {

                // Log connection acquired
                LOGGER.debug("Client ready");

                // Perform as many query mixes as requested
                final String clientContext = MDC.get(Logging.MDC_CONTEXT);
                try {
                    while (globalMixCounter.getAndDecrement() > 0
                            && timestamp < startTimestamp + maxTime) {
                        // Update MDC context
                        final int mixIndex = localMixCounter.incrementAndGet();
                        final String mixContext = String.format("%s.mix%d", clientContext,
                                mixIndex);
                        MDC.put(Logging.MDC_CONTEXT, mixContext);

                        // Pick up a random input tuple
                        final int index;
                        synchronized (random) {
                            index = random.nextInt(this.inputData.length);
                        }
                        final String inputLine = new String(this.inputData[index], Charsets.UTF_8);
                        final BindingSet input = TestUtil.decode(this.inputVariables, inputLine);

                        // Start building the output tuple adding data identifying this mix
                        final ValueFactory vf = Statements.VALUE_FACTORY;
                        final MapBindingSet output = new MapBindingSet();
                        final long mixStartTimestamp = timestamp;
                        output.addBinding("mix.client", vf.createLiteral(clientId));
                        output.addBinding("mix.index", vf.createLiteral(mixIndex));
                        output.addBinding("mix.input", vf.createLiteral(index));
                        output.addBinding("mix.start", vf.createLiteral(mixStartTimestamp));

                        // Log beginning of query mix
                        LOGGER.debug("Started for input #{}", index);

                        // Evaluate the queries of the mix, augmenting the output tuple
                        for (final Query query : this.queries) {
                            try {
                                MDC.put(Logging.MDC_CONTEXT,
                                        String.format("%s.%s", mixContext, query.getName()));
                                final MapBindingSet queryOutput = new MapBindingSet();
                                timestamp = query.evaluate(session, timestamp, input, queryOutput,
                                        stats);
                                for (final Binding binding : queryOutput) {
                                    output.addBinding(query.getName() + "." + binding.getName(),
                                            binding.getValue());
                                }
                            } finally {
                                MDC.put(Logging.MDC_CONTEXT, mixContext);
                            }
                        }
                        final long elapsed = timestamp - mixStartTimestamp;

                        // Log completion of query mix
                        LOGGER.debug("Completed in {} ms", elapsed);
                        if (tracker != null) {
                            tracker.increment();
                        }

                        // Store query mix time and update associated statistics, if supplied
                        output.addBinding("mix.time", vf.createLiteral(elapsed));
                        if (stats != null) {
                            stats.reportQueryMixCompletion(elapsed);
                        }

                        // Emit the output tuple if a Writer has been supplied
                        if (writer != null) {
                            LOGGER.trace("Emitting:\n{}",
                                    TestUtil.format(this.outputVariables, output, "\n"));
                            final String outputLine = TestUtil
                                    .encode(this.outputVariables, output);
                            synchronized (writer) {
                                writer.write(outputLine);
                                writer.write("\n");
                            }
                        }
                    }
                } finally {
                    MDC.put(Logging.MDC_CONTEXT, clientContext);
                }
            }
        }

        // Log end
        LOGGER.debug("Client terminated ({} query mixes)", localMixCounter.get());

        // Return end timestamp of last executed query
        return timestamp;
    }

    private static abstract class Query {

        private static final Logger LOGGER = LoggerFactory.getLogger(Query.class);

        private final String name;

        private final Long timeout;

        private final Set inputVariables;

        private final Set outputVariables;

        Query(final String name, final Properties properties,
                final Iterable inputVariables, final Iterable outputVariables) {

            final String timeout = properties.getProperty("timeout");

            this.name = name;
            this.timeout = timeout != null ? Long.parseLong(timeout) : null;
            this.inputVariables = ImmutableSet.copyOf(inputVariables);
            this.outputVariables = ImmutableSet.copyOf(Iterables.concat( //
                    ImmutableSet.of("start", "time", "error"), outputVariables));
        }

        public static List create(final Properties properties,
                final Properties defaultQueryProperties) {

            final Map map = Maps.newLinkedHashMap();
            for (final Object key : properties.keySet()) {
                final String keyString = key.toString();
                final int index = keyString.indexOf(".");
                if (index > 0) {
                    final String queryName = keyString.substring(0, index);
                    final String propertyName = keyString.substring(index + 1);
                    final String propertyValue = properties.getProperty(keyString);
                    Properties queryProperties = map.get(queryName);
                    if (queryProperties == null) {
                        queryProperties = new Properties();
                        queryProperties.putAll(defaultQueryProperties);
                        map.put(queryName, queryProperties);
                    }
                    queryProperties.setProperty(propertyName, propertyValue);
                }
            }

            final List queries = Lists.newArrayList();
            for (final Map.Entry entry : map.entrySet()) {
                final String queryName = entry.getKey();
                final Properties queryProperties = entry.getValue();
                final String queryType = queryProperties.getProperty("type");
                if ("download".equalsIgnoreCase(queryType)) {
                    queries.add(new DownloadQuery(queryName, queryProperties));
                } else if ("retrieve".equalsIgnoreCase(queryType)) {
                    queries.add(new RetrieveQuery(queryName, queryProperties));
                } else if ("lookup".equalsIgnoreCase(queryType)) {
                    queries.add(new LookupQuery(queryName, queryProperties));
                } else if ("lookupall".equalsIgnoreCase(queryType)) {
                    queries.add(new LookupAllQuery(queryName, queryProperties));
                } else if ("count".equalsIgnoreCase(queryType)) {
                    queries.add(new CountQuery(queryName, queryProperties));
                } else if ("sparql".equalsIgnoreCase(queryType)) {
                    queries.add(new SparqlQuery(queryName, queryProperties));
                }
            }
            return queries;
        }

        public String getName() {
            return this.name;
        }

        public Long getTimeout() {
            return this.timeout;
        }

        public Set getInputVariables() {
            return this.inputVariables;
        }

        public Set getOutputVariables() {
            return this.outputVariables;
        }

        public long evaluate(final Session session, final long startTimestamp,
                final BindingSet input, final MapBindingSet output,
                @Nullable final Statistics stats) {

            final ValueFactory vf = ValueFactoryImpl.getInstance();

            if (LOGGER.isDebugEnabled()) {
                final StringBuilder builder = new StringBuilder();
                builder.append("Started: ");
                builder.append(TestUtil.format(this.inputVariables, input, " "));
                LOGGER.debug(builder.toString());
            }

            String error = "";
            try {
                doEvaluate(session, input, output);
            } catch (final Throwable ex) {
                error = ex.getClass().getSimpleName() + " - "
                        + Strings.nullToEmpty(ex.getMessage());
                LOGGER.warn("Got exception", ex);
            }

            long size = -1;
            try {
                final Value value = output.getValue("size");
                if (value != null) {
                    size = Long.parseLong(value.stringValue());
                }
            } catch (final Throwable ex) {
                // Ignore
            }

            final long endTimestamp = System.currentTimeMillis();
            final long elapsed = endTimestamp - startTimestamp;

            if (stats != null) {
                stats.reportQueryCompletion(this.name, !"".equals(error), elapsed, size);
            }

            output.addBinding("time", vf.createLiteral(elapsed));

            if (LOGGER.isDebugEnabled()) {
                final StringBuilder builder = new StringBuilder();
                builder.append("".equals(error) ? "Success" : "Failure");
                builder.append(": ");
                builder.append(TestUtil.format(this.inputVariables, input, " "));
                builder.append(" -> ");
                builder.append(TestUtil.format(this.outputVariables, output, " "));
                LOGGER.debug(builder.toString());
            }

            output.addBinding("start", vf.createLiteral(startTimestamp));
            output.addBinding("error", vf.createLiteral(error));

            return endTimestamp;
        }

        abstract void doEvaluate(Session session, BindingSet input, MapBindingSet output)
                throws Throwable;

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

        private static class DownloadQuery extends Query {

            private final Template id;

            private final boolean caching;

            DownloadQuery(final String name, final Properties properties) {
                this(name, properties, new Template(properties.getProperty("id")));
            }

            private DownloadQuery(final String name, final Properties properties, final Template id) {
                super(name, properties, id.getVariables(), ImmutableList.of("size"));
                this.id = id;
                this.caching = "false".equalsIgnoreCase(properties.getProperty("caching"));
            }

            @Override
            void doEvaluate(final Session session, final BindingSet input,
                    final MapBindingSet output) throws Throwable {

                final URI id = (URI) Statements.parseValue(this.id.instantiate(input),
                        Namespaces.DEFAULT);

                long size = 0L;
                try (final Representation representation = session.download(id)
                        .caching(this.caching).timeout(getTimeout()).exec()) {
                    if (representation != null) {
                        size = representation.writeToByteArray().length;
                    } else {
                        LOGGER.warn("No results for DOWNLOAD request, id " + id);
                    }
                } catch (final Throwable ex) {
                    throw new RuntimeException("Failed DOWNLOAD, id " + TestUtil.format(id)
                            + ", caching " + this.caching, ex);
                } finally {
                    output.addBinding("size", FACTORY.createLiteral(size));
                }
            }
        }

        private static class RetrieveQuery extends Query {

            private final URI layer;

            @Nullable
            private final Template condition;

            @Nullable
            private final Long offset;

            @Nullable
            private final Long limit;

            @Nullable
            private final List properties;

            RetrieveQuery(final String name, final Properties properties) {
                this(name, properties, Template.forString(properties.getProperty("condition")));
            }

            private RetrieveQuery(final String name, final Properties properties,
                    @Nullable final Template condition) {

                super(name, properties, condition.getVariables(), ImmutableList.of("size"));

                final String offset = properties.getProperty("offset");
                final String limit = properties.getProperty("limit");

                List props = null;
                if (properties.containsKey("properties")) {
                    props = Lists.newArrayList();
                    for (final String token : Splitter.onPattern("[ ,;]").omitEmptyStrings()
                            .trimResults().split(properties.getProperty("properties"))) {
                        props.add((URI) Statements.parseValue(token));
                    }
                }

                this.layer = (URI) Statements.parseValue(properties.getProperty("layer"),
                        Namespaces.DEFAULT);
                this.condition = condition;
                this.offset = offset == null ? null : Long.parseLong(offset);
                this.limit = limit == null ? null : Long.parseLong(limit);
                this.properties = props;
            }

            @Override
            void doEvaluate(final Session session, final BindingSet input,
                    final MapBindingSet output) throws Throwable {

                final String condition = Strings.nullToEmpty(this.condition.instantiate(input));

                long numTriples = 0L;
                try {
                    // FIXME: conditions do not seem to work
                    final Stream stream = session.retrieve(this.layer)
                            .condition(condition).offset(this.offset).limit(this.limit)
                            .properties(this.properties).exec();
                    numTriples = Record.encode(stream, ImmutableList.of(this.layer)).count();
                    if (numTriples == 0) {
                        LOGGER.warn("No results for RETRIEVE request, layer "
                                + TestUtil.format(this.layer) + ", condition '" + condition
                                + "', offset " + this.offset + ", limit " + this.limit);
                    }

                } catch (final Throwable ex) {
                    throw new RuntimeException("Failed RETRIEVE " + TestUtil.format(this.layer)
                            + ", condition " + condition + ", offset " + this.offset + ", limit"
                            + this.limit + ", properties " + this.properties, ex);
                } finally {
                    output.addBinding("size", FACTORY.createLiteral(numTriples));
                }
            }
        }

        private static class LookupQuery extends Query {

            private final URI layer;

            @Nullable
            private final Template id;

            @Nullable
            private final List properties;

            LookupQuery(final String name, final Properties properties) {
                this(name, properties, Template.forString(properties.getProperty("id")));
            }

            private LookupQuery(final String name, final Properties properties,
                    @Nullable final Template id) {

                super(name, properties, id.getVariables(), ImmutableList.of("size"));

                List props = null;
                if (properties.containsKey("properties")) {
                    props = Lists.newArrayList();
                    for (final String token : Splitter.onPattern("[ ,;]").omitEmptyStrings()
                            .trimResults().split(properties.getProperty("properties"))) {
                        props.add((URI) Statements.parseValue(token));
                    }
                }

                this.layer = (URI) Statements.parseValue(properties.getProperty("layer"),
                        Namespaces.DEFAULT);
                this.id = id;
                this.properties = props;
            }

            @Override
            void doEvaluate(final Session session, final BindingSet input,
                    final MapBindingSet output) throws Throwable {

                final URI id = (URI) Statements.parseValue(this.id.instantiate(input),
                        Namespaces.DEFAULT);

                long numTriples = 0L;
                try {
                    final Stream stream = session.retrieve(this.layer).ids(id)
                            .properties(this.properties).exec();
                    numTriples = Record.encode(stream, ImmutableList.of(this.layer)).count();
                    if (numTriples == 0) {
                        LOGGER.warn("No results for LOOKUP request, layer "
                                + TestUtil.format(this.layer) + ", id " + id);
                    }

                } catch (final Throwable ex) {
                    throw new RuntimeException("Failed LOOKUP " + TestUtil.format(this.layer)
                            + ", id " + TestUtil.format(id) + ", properties " + this.properties,
                            ex);
                } finally {
                    output.addBinding("size", FACTORY.createLiteral(numTriples));
                }
            }

        }

        private static class LookupAllQuery extends Query {

            @Nullable
            private final Template id;

            LookupAllQuery(final String name, final Properties properties) {
                this(name, properties, Template.forString(properties.getProperty("id")));
            }

            private LookupAllQuery(final String name, final Properties properties,
                    @Nullable final Template id) {
                super(name, properties, id.getVariables(), ImmutableList.of("size"));
                this.id = id;
            }

            @Override
            void doEvaluate(final Session session, final BindingSet input,
                    final MapBindingSet output) throws Throwable {

                final URI id = (URI) Statements.parseValue(this.id.instantiate(input),
                        Namespaces.DEFAULT);

                long numTriples = 0L;
                try {
                    numTriples += Record.encode(session.retrieve(KS.RESOURCE).ids(id).exec(),
                            ImmutableList.of(KS.RESOURCE)).count();
                    numTriples += Record.encode(
                            session.retrieve(KS.MENTION).condition("ks:mentionOf = $$", id)
                                    .limit(100000L).exec(), ImmutableList.of(KS.MENTION)).count();
                    if (numTriples == 0) {
                        LOGGER.warn("No results for LOOKUP ALL request, id " + id);
                    }
                } catch (final Throwable ex) {
                    throw new RuntimeException("Failed LOOKUP ALL, id " + TestUtil.format(id), ex);
                } finally {
                    output.addBinding("size", FACTORY.createLiteral(numTriples));
                }
            }
        }

        private static class CountQuery extends Query {

            private final URI layer;

            @Nullable
            private final Template condition;

            CountQuery(final String name, final Properties properties) {
                this(name, properties, Template.forString(properties.getProperty("condition")));
            }

            private CountQuery(final String name, final Properties properties,
                    @Nullable final Template condition) {

                super(name, properties, condition.getVariables(), ImmutableList.of("size"));

                this.layer = (URI) Statements.parseValue(properties.getProperty("layer"));
                this.condition = condition;
            }

            @Override
            void doEvaluate(final Session session, final BindingSet input,
                    final MapBindingSet output) throws Throwable {

                final String condition = Strings.nullToEmpty(this.condition.instantiate(input));
                long numResults = 0L;
                try {
                    numResults = session.count(this.layer).condition(condition).exec();
                    if (numResults == 0) {
                        LOGGER.warn("No results for COUNT request, layer "
                                + TestUtil.format(this.layer) + ", condition '" + condition + "'");
                    }
                } catch (final Throwable ex) {
                    throw new RuntimeException("Count " + TestUtil.format(this.layer) + " where "
                            + condition + " failed", ex);
                } finally {
                    output.addBinding("size", FACTORY.createLiteral(numResults));
                }
            }

        }

        private static final class SparqlQuery extends Query {

            private final Template query;

            private final String form;

            SparqlQuery(final String name, final Properties properties) {
                this(name, properties, Template.forString(properties.getProperty("query")));
            }

            private SparqlQuery(final String name, final Properties properties,
                    final Template query) {
                super(name, properties, query.getVariables(), ImmutableList.of("size"));
                this.query = query;
                this.form = detectQueryForm(query.getText());
            }

            private static String detectQueryForm(final String query) {

                final int length = query.length();

                int start = 0;
                while (start < length) {
                    final char ch = query.charAt(start);
                    if (ch == '#') { // comment
                        while (start < length && query.charAt(start) != '\n') {
                            ++start;
                        }
                    } else if (ch == 'p' || ch == 'b' || ch == 'P' || ch == 'B') { // prefix/base
                        while (start < length && query.charAt(start) != '>') {
                            ++start;
                        }
                    } else if (!Character.isWhitespace(ch)) { // found
                        break;
                    }
                    ++start;
                }

                for (int i = start; i < query.length(); ++i) {
                    final char ch = query.charAt(i);
                    if (Character.isWhitespace(ch)) {
                        final String form = query.substring(start, i).toLowerCase();
                        if (!"select".equals(form) && !"construct".equals(form)
                                && !"describe".equals(form) && !"ask".equals(form)) {
                            throw new IllegalArgumentException("Unknown query form: " + form);
                        }
                        return form;
                    }
                }

                throw new IllegalArgumentException("Cannot detect query form");
            }

            @Override
            void doEvaluate(final Session session, final BindingSet input,
                    final MapBindingSet output) throws Throwable {

                long numResults = 0;
                final String queryString = this.query.instantiate(input);
                final Sparql operation = session.sparql(queryString).timeout(getTimeout());

                try {
                    switch (this.form) {
                    case "select":
                        numResults = operation.execTuples().count();
                        break;
                    case "construct":
                    case "describe":
                        numResults = operation.execTriples().count();
                        break;
                    case "ask":
                        operation.execBoolean();
                        numResults = 1;
                        break;
                    default:
                        throw new Error();
                    }
                    if (numResults == 0) {
                        LOGGER.warn("No results for SPARQL request, query is\n" + queryString);
                    }
                    output.addBinding("size", FACTORY.createLiteral(numResults));
                } catch (final Throwable ex) {
                    throw new RuntimeException("Failed SPARQL, form " + this.form.toUpperCase()
                            + ", query:\n" + queryString, ex);
                } finally {
                }
            }

        }

        private static final class Template {

            private static final Pattern PATTERN = Pattern.compile("\\$\\{([^\\}]+)\\}");

            private static Template EMPTY = new Template("");

            private final String text;

            private final String[] placeholderVariables;

            private final Set variables;

            private Template(final String string) {
                Preconditions.checkNotNull(string);
                final List variables = Lists.newArrayList();
                final StringBuilder builder = new StringBuilder();
                final Matcher matcher = PATTERN.matcher(string);
                int offset = 0;
                while (matcher.find()) {
                    builder.append(string.substring(offset, matcher.start()).replace("%", "%%"));
                    builder.append("%s");
                    variables.add(matcher.group(1));
                    offset = matcher.end();
                }
                builder.append(string.substring(offset).replace("%", "%%"));
                this.text = builder.toString();
                this.placeholderVariables = variables.toArray(new String[variables.size()]);
                this.variables = ImmutableSet.copyOf(variables);
            }

            static Template forString(@Nullable final String string) {
                return string == null ? EMPTY : new Template(string);
            }

            String getText() {
                return this.text;
            }

            Set getVariables() {
                return this.variables;
            }

            String instantiate(final BindingSet bindings) {
                final Object[] placeholderValues = new String[this.placeholderVariables.length];
                for (int i = 0; i < placeholderValues.length; ++i) {
                    final Value value = bindings.getValue(this.placeholderVariables[i]);
                    placeholderValues[i] = Data.toString(value, null);
                }
                return String.format(this.text, placeholderValues);
            }

        }

    }

    private static final class Statistics {

        private static final String EMPTY = String.format("%-8s", "");

        private final DescriptiveStatistics queryMixTime;

        private final Map queryInfos;

        private final QueryInfo globalInfo;

        private long elapsedTime;

        public Statistics(final Iterable queryNames) {
            this.queryMixTime = new DescriptiveStatistics();
            this.queryInfos = Maps.newLinkedHashMap();
            this.globalInfo = new QueryInfo();
            this.elapsedTime = 0L;
            for (final String queryName : queryNames) {
                this.queryInfos.put(queryName, new QueryInfo());
            }
        }

        public synchronized void reportQueryCompletion(final String queryName,
                final boolean failure, final long time, final long size) {
            final QueryInfo info = this.queryInfos.get(queryName);
            info.time.addValue(time);
            this.globalInfo.time.addValue(time);
            if (size >= 0) {
                info.size.addValue(size);
                this.globalInfo.size.addValue(size);
            }
            if (failure) {
                ++info.numFailures;
                ++this.globalInfo.numFailures;
            }
        }

        public synchronized void reportQueryMixCompletion(final long time) {
            this.queryMixTime.addValue(time);
        }

        public synchronized void reportElapsedTest(final long elapsedTime) {
            this.elapsedTime = elapsedTime;
        }

        @Override
        public synchronized String toString() {

            // Compute sum of query execution times
            long testTotalTime = 0;
            for (final QueryInfo info : this.queryInfos.values()) {
                testTotalTime += (long) info.time.getSum();
            }

            // Create and return a statistics table
            final StringBuilder builder = new StringBuilder();
            emitHeader(builder);
            emitSeparator(builder);
            for (final Map.Entry entry : this.queryInfos.entrySet()) {
                emitStats(builder, testTotalTime, entry.getKey(), entry.getValue());
            }
            emitSeparator(builder);
            emitStats(builder, testTotalTime, "query (avg)", this.globalInfo);
            emitSeparator(builder);
            emitStats(builder, testTotalTime, "query mix", (int) this.queryMixTime.getN(), -1,
                    null, this.queryMixTime);
            return builder.toString();
        }

        private void emitHeader(final StringBuilder builder) {

            builder.append(String.format("%-12s%-16s%-64s%-64s%-24s%-16s\n", "", "   Executions",
                    "     Result size [solutions, triples or bytes]", "     Execution time [ms]",
                    "     Total time [ms]", "    Rate"));

            builder.append(Strings.repeat(" ", 12));
            for (final String field : new String[] { "Total", "Error", "Min", "Q1", "Q2", "Q3",
                    "Max", "Geom", "Mean", "Std", "Min", "Q1", "Q2", "Q3", "Max", "Geom", "Mean",
                    "Std", "Sum", "Clock", "Share", "/Sec", "/Hour" }) {
                builder.append(String.format("%8s", field));
            }
            builder.append("\n");
        }

        private void emitSeparator(final StringBuilder builder) {
            builder.append(Strings.repeat("-", 8 * 23 + 12)).append("\n");
        }

        private void emitStats(final StringBuilder builder, final long testTotalTime,
                final String label, final QueryInfo info) {
            emitStats(builder, testTotalTime, label, (int) info.time.getN(), info.numFailures,
                    info.size, info.time);
        }

        private void emitStats(final StringBuilder builder, final long testTotalTime,
                final String label, final int numSuccesses, final int numFailures,
                @Nullable final DescriptiveStatistics size, final DescriptiveStatistics time) {

            builder.append(String.format("%-12s", label));

            builder.append(numSuccesses >= 0 ? String.format("%8d", numSuccesses) : EMPTY);
            builder.append(numFailures >= 0 ? String.format("%8d", numFailures) : EMPTY);

            if (size != null) {
                builder.append(String.format("%8d%8d%8d%8d%8d%8.0f%8.0f%8.0f",
                        (long) size.getMin(), (long) size.getPercentile(25),
                        (long) size.getPercentile(50), (long) size.getPercentile(75),
                        (long) size.getMax(), size.getGeometricMean(), size.getMean(),
                        size.getStandardDeviation()));
            } else {
                builder.append(Strings.repeat(EMPTY, 8));
            }

            if (time != null) {
                final long queryTotalTime = (long) time.getSum();
                final double share = (double) queryTotalTime / testTotalTime;
                final long elapsed = (long) (this.elapsedTime * share);
                final double rate = 1000.0 * time.getN() / elapsed;

                builder.append(String.format("%8d%8d%8d%8d%8d%8.0f%8.0f%8.0f",
                        (long) time.getMin(), (long) time.getPercentile(25),
                        (long) time.getPercentile(50), (long) time.getPercentile(75),
                        (long) time.getMax(), time.getGeometricMean(), time.getMean(),
                        time.getStandardDeviation()));

                builder.append(String.format("%8d%8d%8.2f", queryTotalTime, elapsed, share));
                builder.append(String.format("%8.2f%8.0f", rate, rate * 3600));

            } else {
                builder.append(Strings.repeat(EMPTY, 13));
            }

            builder.append("\n");
        }

        private static class QueryInfo {

            public final DescriptiveStatistics time = new DescriptiveStatistics();

            public final DescriptiveStatistics size = new DescriptiveStatistics();

            public int numFailures;

        }

    }

}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy