-
Notifications
You must be signed in to change notification settings - Fork 470
refactor(profiling): store memalloc samples as native objects #15372
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
base: main
Are you sure you want to change the base?
Conversation
|
|
| assert "No samples found" in str(e) | ||
|
|
||
|
|
||
| @pytest.mark.skip(reason="Temporarily suppressed - crashes with large number of samples") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@nsrip-dd What does this test do, and why is it around
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Are you asking about test_heap_profiler_large_heap_overhead or is this comment referencing test_memory_collector_ignore_profiler we discussed yesterday?
If it's about test_heap_profiler_large_heap_overhead, see #12394 which added the test (the blame view helps here) and #12286. In short, that test demonstrated significant overhead prior to switching to a hash map in the heap profiler, and it is a regression test for a crash when we incorrectly used 16-bit indices in some places. The second thing should have been linked in the PR that added the test, but oh well.
BTW the comment at the beggining of the tests is stale; we haven't skipped this test in a while.
| "pymalloc", | ||
| "malloc_debug", | ||
| "pymalloc_debug", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
TODO roll this back when ready, its just to make tests run faster for now
| # Note: events are now exported directly to pprof, so we return empty samples | ||
| return tuple() | ||
|
|
||
| def test_snapshot(self) -> Tuple[MemorySample, ...]: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this is really only necessary for the legacy v1 tests, can remove once those are gone
| /* True if this sample has been reported previously */ | ||
| bool reported; | ||
| /* Count of allocations this sample represents (for scaling) */ | ||
| size_t count; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this may not be needed anymore
| * tracking. We're already inside a reentrancy guard and GC is disabled, so it's safe | ||
| * to call Python functions here (similar to how we call PyUnicode_AsUTF8String for frames). | ||
| */ | ||
| static void |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can someone who understands python refcounting make sure this is doing the right things?
Bootstrap import analysisComparison of import times between this PR and base. SummaryThe average import time from this PR is: 264 ± 4 ms. The average import time from base is: 273 ± 6 ms. The import time difference between this PR and base is: -9.1 ± 0.2 ms. Import time breakdownThe following import paths have shrunk:
|
| if (name_ptr != NULL) { | ||
| Py_ssize_t name_len = PyBytes_Size(name_bytes); | ||
| // Store the thread name in a std::string | ||
| thread_name = std::string(name_ptr, name_len); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this can probably be a string_view
| frames.emplace_back(pyframe); | ||
| } | ||
| // Validate Sample object is in a valid state before use | ||
| if (max_nframe == 0) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
not sure what value this adds
|
|
||
| // Now safe to release the bytes objects since push_frame has copied the strings | ||
| Py_XDECREF(name_bytes); | ||
| Py_XDECREF(filename_bytes); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
if someone who understands python refcounting could confirm that that would be great
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
PyUnicode_AsUTF8String returns a new reference so when no longer needed the refcount needs to be decremented
| tb->reported = true; | ||
| } | ||
| return nullptr; | ||
| delete tb; // Safe to delete nullptr |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
instead of new/delete, consider keeping a pool of these around
| /* This should not happen. It means we did not properly remove a previously-tracked | ||
| * allocation from the map. This should probably be an assertion. Return the previous | ||
| * entry as it is for an allocation that has been freed. */ | ||
| * allocation from the map. This should probably be an assertion. Delete the previous |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
consider adding that assertion
Performance SLOsComparing candidate dsn/traceback-sample (536df5f) with baseline main (9739b2f) 📈 Performance Regressions (1 suite)📈 iastaspects - 118/118✅ add_aspectTime: ✅ 0.407µs (SLO: <10.000µs 📉 -95.9%) vs baseline: +0.9% Memory: ✅ 39.911MB (SLO: <41.500MB -3.8%) vs baseline: +6.1% ✅ add_inplace_aspectTime: ✅ 0.403µs (SLO: <10.000µs 📉 -96.0%) vs baseline: -1.2% Memory: ✅ 39.793MB (SLO: <41.500MB -4.1%) vs baseline: +5.7% ✅ add_inplace_noaspectTime: ✅ 0.316µs (SLO: <10.000µs 📉 -96.8%) vs baseline: -2.2% Memory: ✅ 39.558MB (SLO: <41.500MB -4.7%) vs baseline: +5.0% ✅ add_noaspectTime: ✅ 0.284µs (SLO: <10.000µs 📉 -97.2%) vs baseline: +2.7% Memory: ✅ 39.675MB (SLO: <41.500MB -4.4%) vs baseline: +5.2% ✅ bytearray_aspectTime: ✅ 1.316µs (SLO: <10.000µs 📉 -86.8%) vs baseline: -1.9% Memory: ✅ 39.558MB (SLO: <41.500MB -4.7%) vs baseline: +5.0% ✅ bytearray_extend_aspectTime: ✅ 1.540µs (SLO: <10.000µs 📉 -84.6%) vs baseline: +0.7% Memory: ✅ 39.499MB (SLO: <41.500MB -4.8%) vs baseline: +5.0% ✅ bytearray_extend_noaspectTime: ✅ 0.617µs (SLO: <10.000µs 📉 -93.8%) vs baseline: +1.3% Memory: ✅ 39.459MB (SLO: <41.500MB -4.9%) vs baseline: +4.8% ✅ bytearray_noaspectTime: ✅ 0.484µs (SLO: <10.000µs 📉 -95.2%) vs baseline: +0.7% Memory: ✅ 39.558MB (SLO: <41.500MB -4.7%) vs baseline: +5.0% ✅ bytes_aspectTime: ✅ 1.293µs (SLO: <10.000µs 📉 -87.1%) vs baseline: +0.4% Memory: ✅ 39.872MB (SLO: <41.500MB -3.9%) vs baseline: +6.0% ✅ bytes_noaspectTime: ✅ 0.493µs (SLO: <10.000µs 📉 -95.1%) vs baseline: -0.2% Memory: ✅ 39.793MB (SLO: <41.500MB -4.1%) vs baseline: +5.9% ✅ bytesio_aspectTime: ✅ 1.295µs (SLO: <10.000µs 📉 -87.1%) vs baseline: -0.7% Memory: ✅ 39.577MB (SLO: <41.500MB -4.6%) vs baseline: +5.2% ✅ bytesio_noaspectTime: ✅ 0.493µs (SLO: <10.000µs 📉 -95.1%) vs baseline: -0.7% Memory: ✅ 39.911MB (SLO: <41.500MB -3.8%) vs baseline: +6.1% ✅ capitalize_aspectTime: ✅ 0.733µs (SLO: <10.000µs 📉 -92.7%) vs baseline: -1.0% Memory: ✅ 39.341MB (SLO: <41.500MB -5.2%) vs baseline: +4.7% ✅ capitalize_noaspectTime: ✅ 0.436µs (SLO: <10.000µs 📉 -95.6%) vs baseline: +0.5% Memory: ✅ 39.440MB (SLO: <41.500MB -5.0%) vs baseline: +4.9% ✅ casefold_aspectTime: ✅ 0.732µs (SLO: <10.000µs 📉 -92.7%) vs baseline: -0.2% Memory: ✅ 39.518MB (SLO: <41.500MB -4.8%) vs baseline: +4.8% ✅ casefold_noaspectTime: ✅ 0.371µs (SLO: <10.000µs 📉 -96.3%) vs baseline: -1.0% Memory: ✅ 39.852MB (SLO: <41.500MB -4.0%) vs baseline: +5.8% ✅ decode_aspectTime: ✅ 0.731µs (SLO: <10.000µs 📉 -92.7%) vs baseline: +1.5% Memory: ✅ 39.361MB (SLO: <41.500MB -5.2%) vs baseline: +4.5% ✅ decode_noaspectTime: ✅ 0.423µs (SLO: <10.000µs 📉 -95.8%) vs baseline: +1.5% Memory: ✅ 39.892MB (SLO: <41.500MB -3.9%) vs baseline: +5.8% ✅ encode_aspectTime: ✅ 0.710µs (SLO: <10.000µs 📉 -92.9%) vs baseline: -0.3% Memory: ✅ 39.499MB (SLO: <41.500MB -4.8%) vs baseline: +5.0% ✅ encode_noaspectTime: ✅ 0.406µs (SLO: <10.000µs 📉 -95.9%) vs baseline: +0.9% Memory: ✅ 39.793MB (SLO: <41.500MB -4.1%) vs baseline: +5.8% ✅ format_aspectTime: ✅ 3.499µs (SLO: <10.000µs 📉 -65.0%) vs baseline: +4.9% Memory: ✅ 39.872MB (SLO: <41.500MB -3.9%) vs baseline: +6.1% ✅ format_map_aspectTime: ✅ 3.557µs (SLO: <10.000µs 📉 -64.4%) vs baseline: +2.1% Memory: ✅ 39.518MB (SLO: <41.500MB -4.8%) vs baseline: +4.8% ✅ format_map_noaspectTime: ✅ 0.784µs (SLO: <10.000µs 📉 -92.2%) vs baseline: +1.5% Memory: ✅ 39.479MB (SLO: <41.500MB -4.9%) vs baseline: +5.0% ✅ format_noaspectTime: ✅ 0.595µs (SLO: <10.000µs 📉 -94.1%) vs baseline: ~same Memory: ✅ 39.951MB (SLO: <41.500MB -3.7%) vs baseline: +6.2% ✅ index_aspectTime: ✅ 0.359µs (SLO: <10.000µs 📉 -96.4%) vs baseline: +0.6% Memory: ✅ 39.813MB (SLO: <41.500MB -4.1%) vs baseline: +6.0% ✅ index_noaspectTime: ✅ 0.277µs (SLO: <10.000µs 📉 -97.2%) vs baseline: -0.6% Memory: ✅ 39.931MB (SLO: <41.500MB -3.8%) vs baseline: +6.1% ✅ join_aspectTime: ✅ 1.388µs (SLO: <10.000µs 📉 -86.1%) vs baseline: +0.4% Memory: ✅ 39.833MB (SLO: <41.500MB -4.0%) vs baseline: +5.8% ✅ join_noaspectTime: ✅ 0.489µs (SLO: <10.000µs 📉 -95.1%) vs baseline: -1.0% Memory: ✅ 39.675MB (SLO: <41.500MB -4.4%) vs baseline: +5.5% ✅ ljust_aspectTime: ✅ 2.529µs (SLO: <20.000µs 📉 -87.4%) vs baseline: +0.6% Memory: ✅ 39.892MB (SLO: <41.500MB -3.9%) vs baseline: +6.1% ✅ ljust_noaspectTime: ✅ 0.404µs (SLO: <10.000µs 📉 -96.0%) vs baseline: +1.3% Memory: ✅ 39.754MB (SLO: <41.500MB -4.2%) vs baseline: +5.7% ✅ lower_aspectTime: ✅ 2.195µs (SLO: <10.000µs 📉 -78.0%) vs baseline: -0.3% Memory: ✅ 39.852MB (SLO: <41.500MB -4.0%) vs baseline: +6.0% ✅ lower_noaspectTime: ✅ 0.368µs (SLO: <10.000µs 📉 -96.3%) vs baseline: +1.4% Memory: ✅ 39.951MB (SLO: <41.500MB -3.7%) vs baseline: +6.1% ✅ lstrip_aspectTime: ✅ 2.487µs (SLO: <20.000µs 📉 -87.6%) vs baseline: 📈 +12.3% Memory: ✅ 39.951MB (SLO: <41.500MB -3.7%) vs baseline: +6.1% ✅ lstrip_noaspectTime: ✅ 0.382µs (SLO: <10.000µs 📉 -96.2%) vs baseline: -1.3% Memory: ✅ 39.911MB (SLO: <41.500MB -3.8%) vs baseline: +6.1% ✅ modulo_aspectTime: ✅ 1.052µs (SLO: <10.000µs 📉 -89.5%) vs baseline: +1.5% Memory: ✅ 39.440MB (SLO: <41.500MB -5.0%) vs baseline: +4.9% ✅ modulo_aspect_for_bytearray_bytearrayTime: ✅ 1.555µs (SLO: <10.000µs 📉 -84.5%) vs baseline: -0.2% Memory: ✅ 39.852MB (SLO: <41.500MB -4.0%) vs baseline: +5.9% ✅ modulo_aspect_for_bytesTime: ✅ 0.980µs (SLO: <10.000µs 📉 -90.2%) vs baseline: +0.7% Memory: ✅ 39.911MB (SLO: <41.500MB -3.8%) vs baseline: +5.8% ✅ modulo_aspect_for_bytes_bytearrayTime: ✅ 1.297µs (SLO: <10.000µs 📉 -87.0%) vs baseline: +2.9% Memory: ✅ 39.440MB (SLO: <41.500MB -5.0%) vs baseline: +4.9% ✅ modulo_noaspectTime: ✅ 0.626µs (SLO: <10.000µs 📉 -93.7%) vs baseline: -0.5% Memory: ✅ 39.833MB (SLO: <41.500MB -4.0%) vs baseline: +5.7% ✅ replace_aspectTime: ✅ 5.389µs (SLO: <10.000µs 📉 -46.1%) vs baseline: 📈 +10.2% Memory: ✅ 39.931MB (SLO: <41.500MB -3.8%) vs baseline: +6.3% ✅ replace_noaspectTime: ✅ 0.463µs (SLO: <10.000µs 📉 -95.4%) vs baseline: +0.8% Memory: ✅ 39.813MB (SLO: <41.500MB -4.1%) vs baseline: +5.6% ✅ repr_aspectTime: ✅ 0.907µs (SLO: <10.000µs 📉 -90.9%) vs baseline: -0.1% Memory: ✅ 39.951MB (SLO: <41.500MB -3.7%) vs baseline: +6.3% ✅ repr_noaspectTime: ✅ 0.418µs (SLO: <10.000µs 📉 -95.8%) vs baseline: +1.0% Memory: ✅ 39.754MB (SLO: <41.500MB -4.2%) vs baseline: +5.4% ✅ rstrip_aspectTime: ✅ 1.881µs (SLO: <20.000µs 📉 -90.6%) vs baseline: +1.2% Memory: ✅ 39.872MB (SLO: <41.500MB -3.9%) vs baseline: +6.0% ✅ rstrip_noaspectTime: ✅ 0.380µs (SLO: <10.000µs 📉 -96.2%) vs baseline: -0.7% Memory: ✅ 39.833MB (SLO: <41.500MB -4.0%) vs baseline: +5.7% ✅ slice_aspectTime: ✅ 0.491µs (SLO: <10.000µs 📉 -95.1%) vs baseline: -0.5% Memory: ✅ 39.793MB (SLO: <41.500MB -4.1%) vs baseline: +5.8% ✅ slice_noaspectTime: ✅ 0.450µs (SLO: <10.000µs 📉 -95.5%) vs baseline: ~same Memory: ✅ 39.872MB (SLO: <41.500MB -3.9%) vs baseline: +6.1% ✅ stringio_aspectTime: ✅ 1.537µs (SLO: <10.000µs 📉 -84.6%) vs baseline: ~same Memory: ✅ 39.479MB (SLO: <41.500MB -4.9%) vs baseline: +4.9% ✅ stringio_noaspectTime: ✅ 0.730µs (SLO: <10.000µs 📉 -92.7%) vs baseline: +1.3% Memory: ✅ 40.029MB (SLO: <41.500MB -3.5%) vs baseline: +6.4% ✅ strip_aspectTime: ✅ 2.225µs (SLO: <20.000µs 📉 -88.9%) vs baseline: +1.1% Memory: ✅ 39.852MB (SLO: <41.500MB -4.0%) vs baseline: +6.0% ✅ strip_noaspectTime: ✅ 0.392µs (SLO: <10.000µs 📉 -96.1%) vs baseline: +1.4% Memory: ✅ 39.931MB (SLO: <41.500MB -3.8%) vs baseline: +6.3% ✅ swapcase_aspectTime: ✅ 2.512µs (SLO: <10.000µs 📉 -74.9%) vs baseline: +4.5% Memory: ✅ 39.892MB (SLO: <41.500MB -3.9%) vs baseline: +6.2% ✅ swapcase_noaspectTime: ✅ 0.541µs (SLO: <10.000µs 📉 -94.6%) vs baseline: +1.3% Memory: ✅ 39.872MB (SLO: <41.500MB -3.9%) vs baseline: +5.7% ✅ title_aspectTime: ✅ 2.369µs (SLO: <10.000µs 📉 -76.3%) vs baseline: +1.1% Memory: ✅ 39.813MB (SLO: <41.500MB -4.1%) vs baseline: +5.7% ✅ title_noaspectTime: ✅ 0.503µs (SLO: <10.000µs 📉 -95.0%) vs baseline: ~same Memory: ✅ 39.892MB (SLO: <41.500MB -3.9%) vs baseline: +5.9% ✅ translate_aspectTime: ✅ 3.388µs (SLO: <10.000µs 📉 -66.1%) vs baseline: +5.5% Memory: ✅ 39.911MB (SLO: <41.500MB -3.8%) vs baseline: +6.2% ✅ translate_noaspectTime: ✅ 1.043µs (SLO: <10.000µs 📉 -89.6%) vs baseline: +0.5% Memory: ✅ 39.538MB (SLO: <41.500MB -4.7%) vs baseline: +5.1% ✅ upper_aspectTime: ✅ 2.303µs (SLO: <10.000µs 📉 -77.0%) vs baseline: +4.4% Memory: ✅ 39.793MB (SLO: <41.500MB -4.1%) vs baseline: +5.5% ✅ upper_noaspectTime: ✅ 0.372µs (SLO: <10.000µs 📉 -96.3%) vs baseline: +0.5% Memory: ✅ 39.872MB (SLO: <41.500MB -3.9%) vs baseline: +5.8% 🟡 Near SLO Breach (1 suite)🟡 telemetryaddmetric - 30/30✅ 1-count-metric-1-timesTime: ✅ 2.990µs (SLO: <20.000µs 📉 -85.1%) vs baseline: +3.1% Memory: ✅ 34.682MB (SLO: <35.500MB -2.3%) vs baseline: +5.2% ✅ 1-count-metrics-100-timesTime: ✅ 201.374µs (SLO: <220.000µs -8.5%) vs baseline: +1.0% Memory: ✅ 34.544MB (SLO: <35.500MB -2.7%) vs baseline: +4.9% ✅ 1-distribution-metric-1-timesTime: ✅ 3.563µs (SLO: <20.000µs 📉 -82.2%) vs baseline: +9.6% Memory: ✅ 34.564MB (SLO: <35.500MB -2.6%) vs baseline: +5.1% ✅ 1-distribution-metrics-100-timesTime: ✅ 213.687µs (SLO: <230.000µs -7.1%) vs baseline: -0.9% Memory: ✅ 34.662MB (SLO: <35.500MB -2.4%) vs baseline: +5.1% ✅ 1-gauge-metric-1-timesTime: ✅ 2.174µs (SLO: <20.000µs 📉 -89.1%) vs baseline: +0.5% Memory: ✅ 34.544MB (SLO: <35.500MB -2.7%) vs baseline: +4.6% ✅ 1-gauge-metrics-100-timesTime: ✅ 137.619µs (SLO: <150.000µs -8.3%) vs baseline: +0.6% Memory: ✅ 34.662MB (SLO: <35.500MB -2.4%) vs baseline: +5.0% ✅ 1-rate-metric-1-timesTime: ✅ 3.045µs (SLO: <20.000µs 📉 -84.8%) vs baseline: ~same Memory: ✅ 34.485MB (SLO: <35.500MB -2.9%) vs baseline: +4.3% ✅ 1-rate-metrics-100-timesTime: ✅ 215.342µs (SLO: <250.000µs 📉 -13.9%) vs baseline: +0.7% Memory: ✅ 34.603MB (SLO: <35.500MB -2.5%) vs baseline: +4.9% ✅ 100-count-metrics-100-timesTime: ✅ 20.544ms (SLO: <22.000ms -6.6%) vs baseline: +1.3% Memory: ✅ 34.583MB (SLO: <35.500MB -2.6%) vs baseline: +5.0% ✅ 100-distribution-metrics-100-timesTime: ✅ 2.265ms (SLO: <2.300ms 🟡 -1.5%) vs baseline: +1.3% Memory: ✅ 34.524MB (SLO: <35.500MB -2.7%) vs baseline: +4.8% ✅ 100-gauge-metrics-100-timesTime: ✅ 1.412ms (SLO: <1.550ms -8.9%) vs baseline: +0.2% Memory: ✅ 34.544MB (SLO: <35.500MB -2.7%) vs baseline: +4.7% ✅ 100-rate-metrics-100-timesTime: ✅ 2.249ms (SLO: <2.550ms 📉 -11.8%) vs baseline: +2.5% Memory: ✅ 34.485MB (SLO: <35.500MB -2.9%) vs baseline: +4.3% ✅ flush-1-metricTime: ✅ 4.823µs (SLO: <20.000µs 📉 -75.9%) vs baseline: +8.6% Memory: ✅ 34.544MB (SLO: <35.500MB -2.7%) vs baseline: +5.0% ✅ flush-100-metricsTime: ✅ 172.902µs (SLO: <250.000µs 📉 -30.8%) vs baseline: -0.6% Memory: ✅ 34.485MB (SLO: <35.500MB -2.9%) vs baseline: +4.3% ✅ flush-1000-metricsTime: ✅ 2.123ms (SLO: <2.500ms 📉 -15.1%) vs baseline: -0.2% Memory: ✅ 35.350MB (SLO: <36.500MB -3.2%) vs baseline: +4.7%
|
|
|
||
| Py_INCREF(name); | ||
| Py_INCREF(filename); | ||
| // Get thread native ID from Python |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We can extract the thread tracking logic from echion and make generally available to the profiler, so that we have a convenient way of retrieving thread information within the native layer
| auto it = thread_info_map.find(id); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Awesome. lmk when that's available?
| // Note: Sample.push_frame() comment says it "Assumes frames are pushed in leaf-order", | ||
| // but we push root-to-leaf. Set reverse_locations so the sample will be reversed when exported. | ||
| sample.set_reverse_locations(true); | ||
| for (PyFrameObject* frame = pyframe; frame != NULL;) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should avoid unwinding the stack using PyFrameObjects in versions of CPython where PyInterpreterFrame is available to avoid allocations (see e.g. https://github.com/P403n1x87/echion/blob/b6d048df7d22fba77b7996964817d2bded7ec08e/echion/stacks.h#L211-L223)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also, because we probably hold the GIL here, it wouldn't make sense to ask Echion to give us the call stack because we don't need to make safe copies
Description
Previously, we kept tracebacks in python objects, which meant that dealing with them required careful work with the GIL.
Replacing with native storage makes the code much cleaner.
Testing
Existing tests
Risks
This is a substantial change to how the profiler works, and should be looked at carefully.
Additional Notes