|
@@ -9,6 +9,7 @@ package org.elasticsearch.xpack.esql.qa.single_node;
|
|
|
import com.carrotsearch.randomizedtesting.annotations.ParametersFactory;
|
|
|
import com.carrotsearch.randomizedtesting.annotations.ThreadLeakFilters;
|
|
|
|
|
|
+import org.apache.http.HttpEntity;
|
|
|
import org.apache.http.util.EntityUtils;
|
|
|
import org.apache.lucene.search.DocIdSetIterator;
|
|
|
import org.elasticsearch.Build;
|
|
@@ -17,38 +18,52 @@ import org.elasticsearch.client.Response;
|
|
|
import org.elasticsearch.client.ResponseException;
|
|
|
import org.elasticsearch.common.io.Streams;
|
|
|
import org.elasticsearch.common.settings.Settings;
|
|
|
+import org.elasticsearch.common.xcontent.XContentHelper;
|
|
|
import org.elasticsearch.test.ListMatcher;
|
|
|
import org.elasticsearch.test.MapMatcher;
|
|
|
import org.elasticsearch.test.TestClustersThreadFilter;
|
|
|
import org.elasticsearch.test.cluster.ElasticsearchCluster;
|
|
|
import org.elasticsearch.test.cluster.LogType;
|
|
|
+import org.elasticsearch.xcontent.XContentBuilder;
|
|
|
import org.elasticsearch.xcontent.XContentType;
|
|
|
+import org.elasticsearch.xcontent.json.JsonXContent;
|
|
|
import org.elasticsearch.xpack.esql.qa.rest.RestEsqlTestCase;
|
|
|
+import org.elasticsearch.xpack.esql.tools.ProfileParser;
|
|
|
import org.hamcrest.Matchers;
|
|
|
import org.junit.Assert;
|
|
|
import org.junit.ClassRule;
|
|
|
|
|
|
+import java.io.ByteArrayInputStream;
|
|
|
+import java.io.ByteArrayOutputStream;
|
|
|
import java.io.IOException;
|
|
|
import java.io.InputStream;
|
|
|
import java.nio.charset.StandardCharsets;
|
|
|
import java.util.ArrayList;
|
|
|
import java.util.Arrays;
|
|
|
+import java.util.HashSet;
|
|
|
import java.util.List;
|
|
|
import java.util.Locale;
|
|
|
import java.util.Map;
|
|
|
+import java.util.Set;
|
|
|
|
|
|
import static org.elasticsearch.test.ListMatcher.matchesList;
|
|
|
import static org.elasticsearch.test.MapMatcher.assertMap;
|
|
|
import static org.elasticsearch.test.MapMatcher.matchesMap;
|
|
|
+import static org.elasticsearch.xpack.esql.qa.rest.RestEsqlTestCase.Mode.SYNC;
|
|
|
+import static org.elasticsearch.xpack.esql.tools.ProfileParser.parseProfile;
|
|
|
+import static org.elasticsearch.xpack.esql.tools.ProfileParser.readProfileFromResponse;
|
|
|
import static org.hamcrest.Matchers.any;
|
|
|
import static org.hamcrest.Matchers.containsInAnyOrder;
|
|
|
import static org.hamcrest.Matchers.containsString;
|
|
|
import static org.hamcrest.Matchers.either;
|
|
|
+import static org.hamcrest.Matchers.empty;
|
|
|
import static org.hamcrest.Matchers.equalTo;
|
|
|
import static org.hamcrest.Matchers.greaterThan;
|
|
|
+import static org.hamcrest.Matchers.greaterThanOrEqualTo;
|
|
|
import static org.hamcrest.Matchers.hasItem;
|
|
|
import static org.hamcrest.Matchers.instanceOf;
|
|
|
import static org.hamcrest.Matchers.not;
|
|
|
+import static org.hamcrest.Matchers.oneOf;
|
|
|
import static org.hamcrest.Matchers.startsWith;
|
|
|
import static org.hamcrest.core.Is.is;
|
|
|
|
|
@@ -330,6 +345,116 @@ public class RestEsqlIT extends RestEsqlTestCase {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+ private final String PROCESS_NAME = "process_name";
|
|
|
+ private final String THREAD_NAME = "thread_name";
|
|
|
+
|
|
|
+ @SuppressWarnings("unchecked")
|
|
|
+ public void testProfileParsing() throws IOException {
|
|
|
+ indexTimestampData(1);
|
|
|
+
|
|
|
+ RequestObjectBuilder builder = new RequestObjectBuilder(XContentType.JSON).query(fromIndex() + " | stats avg(value)").profile(true);
|
|
|
+ Request request = prepareRequestWithOptions(builder, SYNC);
|
|
|
+ HttpEntity response = performRequest(request).getEntity();
|
|
|
+
|
|
|
+ ProfileParser.Profile profile;
|
|
|
+ try (InputStream responseContent = response.getContent()) {
|
|
|
+ profile = readProfileFromResponse(responseContent);
|
|
|
+ }
|
|
|
+
|
|
|
+ ByteArrayOutputStream os = new ByteArrayOutputStream();
|
|
|
+ try (XContentBuilder jsonOutputBuilder = new XContentBuilder(JsonXContent.jsonXContent, os)) {
|
|
|
+ parseProfile(profile, jsonOutputBuilder);
|
|
|
+ }
|
|
|
+
|
|
|
+ // Read the written JSON again into a map, so we can make assertions on it
|
|
|
+ ByteArrayInputStream profileJson = new ByteArrayInputStream(os.toByteArray());
|
|
|
+ Map<String, Object> parsedProfile = XContentHelper.convertToMap(JsonXContent.jsonXContent, profileJson, true);
|
|
|
+
|
|
|
+ assertEquals("ns", parsedProfile.get("displayTimeUnit"));
|
|
|
+ List<Map<String, Object>> events = (List<Map<String, Object>>) parsedProfile.get("traceEvents");
|
|
|
+ // At least 1 metadata event to declare the node, and 2 events each for the data, node_reduce and final drivers, resp.
|
|
|
+ assertThat(events.size(), greaterThanOrEqualTo(7));
|
|
|
+
|
|
|
+ String clusterName = "test-cluster";
|
|
|
+ Set<String> expectedProcessNames = new HashSet<>();
|
|
|
+ for (int i = 0; i < cluster.getNumNodes(); i++) {
|
|
|
+ expectedProcessNames.add(clusterName + ":" + cluster.getName(i));
|
|
|
+ }
|
|
|
+
|
|
|
+ int seenNodes = 0;
|
|
|
+ int seenDrivers = 0;
|
|
|
+ // Declaration of each node as a "process" via a metadata event (phase `ph` is `M`)
|
|
|
+ // First event has to declare the first seen node.
|
|
|
+ Map<String, Object> nodeMetadata = events.get(0);
|
|
|
+ assertProcessMetadataForNextNode(nodeMetadata, expectedProcessNames, seenNodes++);
|
|
|
+
|
|
|
+ // The rest should be pairs of 2 events: first, a metadata event, declaring 1 "thread" per driver in the profile, then
|
|
|
+ // a "complete" event (phase `ph` is `X`) with a timestamp, duration `dur`, thread duration `tdur` (cpu time) and additional
|
|
|
+ // arguments obtained from the driver.
|
|
|
+ // Except when run as part of the Serverless tests, which can involve more than 1 node - in which case, there will be more node
|
|
|
+ // metadata events.
|
|
|
+ for (int i = 1; i < events.size() - 1;) {
|
|
|
+ String eventName = (String) events.get(i).get("name");
|
|
|
+ assertTrue(Set.of(THREAD_NAME, PROCESS_NAME).contains(eventName));
|
|
|
+ if (eventName.equals(THREAD_NAME)) {
|
|
|
+ Map<String, Object> metadataEventForDriver = events.get(i);
|
|
|
+ Map<String, Object> eventForDriver = events.get(i + 1);
|
|
|
+ assertDriverData(metadataEventForDriver, eventForDriver, seenNodes, seenDrivers);
|
|
|
+ i = i + 2;
|
|
|
+ seenDrivers++;
|
|
|
+ } else if (eventName.equals(PROCESS_NAME)) {
|
|
|
+ Map<String, Object> metadataEventForNode = events.get(i);
|
|
|
+ assertProcessMetadataForNextNode(metadataEventForNode, expectedProcessNames, seenNodes);
|
|
|
+ i++;
|
|
|
+ seenNodes++;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ @SuppressWarnings("unchecked")
|
|
|
+ public void assertProcessMetadataForNextNode(Map<String, Object> nodeMetadata, Set<String> expectedNamesForNodes, int seenNodes) {
|
|
|
+ assertEquals("M", nodeMetadata.get("ph"));
|
|
|
+ assertEquals(PROCESS_NAME, nodeMetadata.get("name"));
|
|
|
+ assertEquals(seenNodes, nodeMetadata.get("pid"));
|
|
|
+
|
|
|
+ Map<String, Object> nodeMetadataArgs = (Map<String, Object>) nodeMetadata.get("args");
|
|
|
+ assertTrue(expectedNamesForNodes.contains((String) nodeMetadataArgs.get("name")));
|
|
|
+ }
|
|
|
+
|
|
|
+ @SuppressWarnings("unchecked")
|
|
|
+ public void assertDriverData(Map<String, Object> driverMetadata, Map<String, Object> driverEvent, int seenNodes, int seenDrivers) {
|
|
|
+ assertEquals("M", driverMetadata.get("ph"));
|
|
|
+ assertEquals(THREAD_NAME, driverMetadata.get("name"));
|
|
|
+ assertTrue((int) driverMetadata.get("pid") < seenNodes);
|
|
|
+ assertEquals(seenDrivers, driverMetadata.get("tid"));
|
|
|
+ Map<String, Object> driverMetadataArgs = (Map<String, Object>) driverMetadata.get("args");
|
|
|
+ String driverType = (String) driverMetadataArgs.get("name");
|
|
|
+ assertThat(driverType, oneOf("data", "node_reduce", "final"));
|
|
|
+
|
|
|
+ assertEquals("X", driverEvent.get("ph"));
|
|
|
+ assertThat((String) driverEvent.get("name"), startsWith(driverType));
|
|
|
+ // Category used to implicitly colour-code and group drivers
|
|
|
+ assertEquals(driverType, driverEvent.get("cat"));
|
|
|
+ assertTrue((int) driverEvent.get("pid") < seenNodes);
|
|
|
+ assertEquals(seenDrivers, driverEvent.get("tid"));
|
|
|
+ long timestampMillis = (long) driverEvent.get("ts");
|
|
|
+ double durationMicros = (double) driverEvent.get("dur");
|
|
|
+ double cpuDurationMicros = (double) driverEvent.get("tdur");
|
|
|
+ assertTrue(timestampMillis >= 0);
|
|
|
+ assertTrue(durationMicros >= 0);
|
|
|
+ assertTrue(cpuDurationMicros >= 0);
|
|
|
+ assertTrue(durationMicros >= cpuDurationMicros);
|
|
|
+
|
|
|
+ // This should contain the essential information from a driver, like its operators, and will be just attached to the slice/
|
|
|
+ // visible when clicking on it.
|
|
|
+ Map<String, Object> driverSliceArgs = (Map<String, Object>) driverEvent.get("args");
|
|
|
+ assertNotNull(driverSliceArgs.get("cpu_nanos"));
|
|
|
+ assertNotNull(driverSliceArgs.get("took_nanos"));
|
|
|
+ assertNotNull(driverSliceArgs.get("iterations"));
|
|
|
+ assertNotNull(driverSliceArgs.get("sleeps"));
|
|
|
+ assertThat(((List<String>) driverSliceArgs.get("operators")), not(empty()));
|
|
|
+ }
|
|
|
+
|
|
|
public void testProfileOrdinalsGroupingOperator() throws IOException {
|
|
|
assumeTrue("requires pragmas", Build.current().isSnapshot());
|
|
|
indexTimestampData(1);
|