|  | @@ -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);
 |