Skip to content

Profile the fetch phase #77064

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 40 commits into from
Sep 13, 2021
Merged

Profile the fetch phase #77064

merged 40 commits into from
Sep 13, 2021

Conversation

nik9000
Copy link
Member

@nik9000 nik9000 commented Aug 31, 2021

This adds profiling to the fetch phase so we can tell when fetching is
slower than we'd like and we can tell which portion of the fetch is
slow. The output includes which stored fields were loaded, how long it
took to load stored fields, which fetch sub-phases were run, and how
long those fetch sub-phases took.

Closes #75892

@nik9000 nik9000 added >feature :Search/Search Search-related issues that do not fall into other categories v8.0.0 v7.16.0 labels Aug 31, 2021
Copy link
Member Author

@nik9000 nik9000 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I implemented this by having the fetch phase optionally profile itself, attaching the profile results to the per-shard fetch results. I then merge those fetch results into the "search phase" profiling that we have been collecting for years. The rendered json of the profile result will only include a fetch section on shards that performed a fetch.

"aggregations": [],
"fetch": {
"type": "fetch",
"description": "fetch",
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This type and description come from my using a raw ProfileResult for the entire fetch phase. I'm not happy about them, but I wasn't sure if it was worth writing a whole top level class for the result or working around them some other way.

"fetch": {
"type": "fetch",
"description": "fetch",
"time_in_nanos": 660555,
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the actual nanoTime for the entire fetch phase rather than the sum of the breakdown. Because we can do that in fetch. We couldn't really do that for aggs or queries.

"load_stored_fields_count": 5
},
"debug": {
"stored_fields": ["_id", "_routing", "_source"]
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This bit right here is important to me. Some folks want to only fetch doc values and avoid stored fields entirely. With this PR you can profile the query and check that this array is empty. If it is then we skipped loading doc values. Unless something tricky is happening in the nested case that I didn't dig into.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like the debug section too!

"children": [
{
"type": "source",
"description": "load _source",
Copy link
Member Author

@nik9000 nik9000 Aug 31, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We get one of these per fetch sub phase we run.

@Override
public String name() {
return "percolator_highlight";
}
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These power the type and description fields on the children field in the fetch profile.

return;
}

Profiler profiler = context.getProfilers() == null ? Profiler.NOOP : context.getProfilers().getFetchProfiler();
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wasn't super happy with how invasive the change was here. I'm happy that the sub-phases for the most part don't have to change which is good, but this bit is bigger than I'd like. I'd love any suggestions if anyone has any.

@@ -428,4 +475,57 @@ private static void fillDocAndMetaFields(SearchContext context, FieldsVisitor fi
static boolean hasSequentialDocs(DocIdToIndex[] docs) {
return docs.length > 0 && docs[docs.length-1].docId - docs[0].docId == docs.length - 1;
}

interface Profiler {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I went with the internal interface and noop implementation to save from a bunch of null checks that'd have made the whole thing even more invasive. I'd be more than happy to get rid of them if anyone has any good ideas.

@SuppressWarnings("unchecked")
private void hitExecute(FetchSourceContext fetchSourceContext, HitContext hitContext) {
@SuppressWarnings("unchecked")
private void hitExecute(FetchSourceContext fetchSourceContext, HitContext hitContext) {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pulled into the phase object so we could collect debugging counters.


@Override
public FetchSubPhaseProcessor getProcessor(FetchContext context) {
if (context.scriptFields() == null) {
if (context.scriptFields() == null || context.scriptFields().fields().isEmpty()) {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When you don't ask for any script fields we tend to get an empty list here, not null. That empty list was causing us to always return the phase and run it. That's not a big deal at all because it just iterates an empty list, but it was clogging up the profile output.

@nik9000 nik9000 force-pushed the profile_fetch_simple branch from faf97a9 to 2c4803d Compare August 31, 2021 14:00
This adds profiling to the fetch phase so we can tell when fetching is
slower than we'd like and we can tell which portion of the fetch is
slow. The output includes which stored fields were loaded, how long it
took to load stored fields, which fetch sub-phases were run, and how
long those fetch sub-phases took.

Closes elastic#75892
@nik9000
Copy link
Member Author

nik9000 commented Aug 31, 2021

** elasticsearch-ci/part-1 ** — Build finished.

I can't reproduce this failure. Its in the CCS dueling tests. Both look like they fetch the same 8 documents but they fetch them from different shards: https://gist.github.com/nik9000/809fa7865e3d5b1c7dd50e96b89e3452

@nik9000
Copy link
Member Author

nik9000 commented Aug 31, 2021

I can't reproduce this failure. Its in the CCS dueling tests. Both look like they fetch the same 8 documents but they fetch them from different shards: https://gist.github.com/nik9000/809fa7865e3d5b1c7dd50e96b89e3452

Nope. I was on the wrong branch. I can reproduce. Checking.

@nik9000
Copy link
Member Author

nik9000 commented Aug 31, 2021

Nope. I was on the wrong branch. I can reproduce. Checking.

OK! It's because the test assumed that the entire search result would be the same when searching in minimize round trip mode vs non-minimized mode. If you minimize the round trips you will perform more fetches so, of course, there will be more fetches.

@nik9000
Copy link
Member Author

nik9000 commented Aug 31, 2021

So folks don't have to dig through the docs, this is what the fetch profile looks like:

        "fetch": {
          "type": "fetch",
          "description": "fetch",
          "time_in_nanos": 660555,
          "breakdown": {
            "next_reader": 7292,
            "next_reader_count": 1,
            "load_stored_fields": 299325,
            "load_stored_fields_count": 5
          },
          "debug": {
            "stored_fields": ["_id", "_routing", "_source"]
          },
          "children": [
            {
              "type": "source",
              "description": "load _source",
              "time_in_nanos": 20443,
              "breakdown": {
                "next_reader": 745,
                "next_reader_count": 1,
                "process": 19698,
                "process_count": 5
              },
              "debug": {
                "loaded_nested": 0,
                "fast_path": 5
              }
            }
          ]
        }

@nik9000
Copy link
Member Author

nik9000 commented Aug 31, 2021

I've found a few more unit tests I'd like to write. Incoming.

@nik9000
Copy link
Member Author

nik9000 commented Aug 31, 2021

I've found a few more unit tests I'd like to write. Incoming.

500 lines of tests..... The profile stuff didn't have any round trip unit tests so I added a bunch. Those are useful to catch serialization issues super early.

@nik9000 nik9000 marked this pull request as ready for review August 31, 2021 21:20
@elasticmachine elasticmachine added the Team:Search Meta label for search team label Aug 31, 2021
@elasticmachine
Copy link
Collaborator

Pinging @elastic/es-search (Team:Search)

Copy link
Contributor

@jtibshirani jtibshirani left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is so close! I had one last comment.

import static org.hamcrest.Matchers.matchesPattern;
import static org.hamcrest.Matchers.nullValue;

public class SearchProfileQueryPhaseResultsTests extends ESTestCase {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this should be named SearchProfileResultsBuilderTests now.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

* Profile results for the query phase run on all shards.
*/
public class SearchProfileResultsBuilder {
public static SearchProfileResultsBuilder build(Map<String, SearchProfileQueryPhaseResult> queryPhaseResults) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am a little confused about this part -- probably related to naming. Could we just have a constructor new SearchProfileResultsBuilder(Map<String, SearchProfileQueryPhaseResult>) instead of this static method? I'm not sure why we need the special NOT_PROFILING object instead of just passing null in the original place in SearchPhaseController.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was trying to get the null check out of search phase controller so it was easy to unit test without all of SearchPhaseController. This way there isn't a null check. I liked that the old code didn't allocate anything if we didn't profile and wanted to keep that too, but have it here.

Another idea is to make the build method static and have it take a nullable builder param. Or just revert the whole change and keep the null check in SearchPhaseController. I just, like, didn't like it there. I'm happy anytime I can get things out of SearchPhaseController.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since this change has so many "moving parts" already, I'd be in favor of keeping the old strategy to keep it well-scoped. I also find it easier to understand. If we want we could follow-up with a refactor to simplify SearchPhaseController a bit.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah. I think it was clearer that way but I'm not the only one that has to read it. I've changed it the way you asked.

Copy link
Contributor

@jtibshirani jtibshirani left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was wrong about that being the last comment. I have a few more small ones.

/**
* Profiles from child-phases.
*/
default List<ProfileResult> childProfiles() {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No current subprocessor overrides this, could we remove until a follow-up where we implement profiling for inner_hits subphases?

try {
List<FetchSubPhaseProcessor> processors = new ArrayList<>();
for (FetchSubPhase fsp : fetchSubPhases) {
FetchSubPhaseProcessor processor = fsp.getProcessor(context);
if (processor != null) {
processors.add(processor);
String type = fsp.getClass().getSimpleName().replaceAll("^Fetch", "").replaceAll("(FetchSub)?Phase$", "");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Personally I think it's nice to simply include the class name without replacements. It's still very readable and makes it easier to find the corresponding code for the phase.

* Profile results for the query phase run on all shards.
*/
public class SearchProfileResultsBuilder {
public static SearchProfileResultsBuilder build(Map<String, SearchProfileQueryPhaseResult> queryPhaseResults) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since this change has so many "moving parts" already, I'd be in favor of keeping the old strategy to keep it well-scoped. I also find it easier to understand. If we want we could follow-up with a refactor to simplify SearchPhaseController a bit.

@nik9000
Copy link
Member Author

nik9000 commented Sep 10, 2021 via email

@nik9000
Copy link
Member Author

nik9000 commented Sep 10, 2021

OK! Back to you @jtibshirani.

Copy link
Contributor

@jtibshirani jtibshirani left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good to me!

@nik9000 nik9000 merged commit c2c0165 into elastic:master Sep 13, 2021
nik9000 added a commit to nik9000/elasticsearch that referenced this pull request Sep 13, 2021
This adds profiling to the fetch phase so we can tell when fetching is
slower than we'd like and we can tell which portion of the fetch is
slow. The output includes which stored fields were loaded, how long it
took to load stored fields, which fetch sub-phases were run, and how
long those fetch sub-phases took.

Closes elastic#75892
nik9000 added a commit to nik9000/elasticsearch that referenced this pull request Sep 13, 2021
In elastic#77064 I added some more tests for profiling. It fails when a
`rarely()` block hits - roughly 2% of the time. This should fix it.
nik9000 added a commit to nik9000/elasticsearch that referenced this pull request Sep 13, 2021
In elastic#77064 I added some more tests for profiling. It fails when a
`rarely()` block hits - roughly 2% of the time. This should fix it.
@nik9000 nik9000 mentioned this pull request Sep 13, 2021
nik9000 added a commit that referenced this pull request Sep 13, 2021
In #77064 I added some more tests for profiling. It fails when a
`rarely()` block hits - roughly 2% of the time. This should fix it.
nik9000 added a commit that referenced this pull request Sep 13, 2021
This adds profiling to the fetch phase so we can tell when fetching is
slower than we'd like and we can tell which portion of the fetch is
slow. The output includes which stored fields were loaded, how long it
took to load stored fields, which fetch sub-phases were run, and how
long those fetch sub-phases took.

Closes #75892
nik9000 added a commit to nik9000/elasticsearch that referenced this pull request Sep 13, 2021
Now that elastic#77064 is backported we can run the bwc tests against it. And
we can speak to 7.16 nodes using the wire protocol addedin elastic#77064.
elasticsearchmachine pushed a commit that referenced this pull request Sep 13, 2021
Now that #77064 is backported we can run the bwc tests against it. And
we can speak to 7.16 nodes using the wire protocol addedin #77064.
@nik9000
Copy link
Member Author

nik9000 commented Sep 13, 2021

In and backported! Exciting!

@javanna
Copy link
Member

javanna commented Apr 19, 2022

Heya, shall we update the limitations from the profile docs page now that fetch can be profiled too? https://www.elastic.co/guide/en/elasticsearch/reference/current/search-profile.html#profile-limitations

@nik9000
Copy link
Member Author

nik9000 commented Jun 6, 2022

Heya, shall we update the limitations from the profile docs page now that fetch can be profiled too? https://www.elastic.co/guide/en/elasticsearch/reference/current/search-profile.html#profile-limitations

I have no idea when, but someone's removed that "limitation".

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
>feature :Search/Search Search-related issues that do not fall into other categories Team:Search Meta label for search team v7.16.0 v8.0.0-beta1
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Search profiling for fetch phases
5 participants