Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions src/coreclr/inc/configuration.h
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,21 @@
#ifndef __configuration_h__
#define __configuration_h__

typedef HRESULT (*config_knob_fn)(
const LPCWSTR& name,
const LPCWSTR& value,
void* context
);

class Configuration
{
public:
static void InitializeConfigurationKnobs(int numberOfConfigs, LPCWSTR *configNames, LPCWSTR *configValues);

// Invokes callback with given context for each configuration knob.
// Returns S_OK for successful enumeration; otherwise HRESULT from failed callback.
static HRESULT EnumerateKnobs(config_knob_fn callback, void* context);

// Returns (in priority order):
// - The value of the ConfigDWORDInfo if it's set
// - The value of the ConfigurationKnob (searched by name) if it's set (performs a u16_strtoul).
Expand Down
7 changes: 7 additions & 0 deletions src/coreclr/nativeaot/Runtime/eventpipe/ds-rt-aot.h
Original file line number Diff line number Diff line change
Expand Up @@ -272,6 +272,13 @@ ds_rt_disable_perfmap (void)
return DS_IPC_E_NOTSUPPORTED;
}

static
uint32_t
ds_rt_appcontext_properties_get (dn_vector_ptr_t *props_array)
{
return DS_IPC_E_NOTSUPPORTED;
}

/*
* DiagnosticServer.
*/
Expand Down
14 changes: 14 additions & 0 deletions src/coreclr/utilcode/configuration.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,20 @@ void Configuration::InitializeConfigurationKnobs(int numberOfConfigs, LPCWSTR *n
knobValues = values;
}

HRESULT Configuration::EnumerateKnobs(config_knob_fn callback, void* context)
{
HRESULT hr = S_OK;

for (int i = 0; i < numberOfKnobs; ++i)
{
_ASSERT(knobNames[i] != nullptr);
if (FAILED(hr = callback(knobNames[i], knobValues[i], context)))
return hr;
}

return S_OK;
}

static LPCWSTR GetConfigurationValue(LPCWSTR name)
{
_ASSERT(name != nullptr);
Expand Down
40 changes: 40 additions & 0 deletions src/coreclr/vm/eventing/eventpipe/ds-rt-coreclr.h
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
#ifdef ENABLE_PERFTRACING
#include "ep-rt-coreclr.h"
#include <clrconfignocache.h>
#include <configuration.h>
#include <eventpipe/ds-process-protocol.h>
#include <eventpipe/ds-profiler-protocol.h>
#include <eventpipe/ds-dump-protocol.h>
Expand Down Expand Up @@ -329,6 +330,45 @@ ds_rt_disable_perfmap (void)
#endif // FEATURE_PERFMAP
}

static
uint32_t
ds_rt_appcontext_properties_get (dn_vector_ptr_t *props_array)
{
STATIC_CONTRACT_NOTHROW;
EP_ASSERT (props_array != NULL);

return Configuration::EnumerateKnobs([](const LPCWSTR& name, const LPCWSTR& value, void* context) {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I've tapped into the Configuration class for this enumeration since it is the owner of the app config properties starting here and the diagnostic server/command handling is started later via here.

The other statically available places to access this information were possibly from the host or coreclr exports and contracts, but I felt that it was not appropriate to add the enumeration API there for external consumption; also, based on discussion in #83756, there may be some desire to filter the properties before the AppContext gets to report them, which feels like it would naturally fit in at configuration initialization. Finally, I will have a follow up PR that allows modification of these values, which the Configuration class can be more easily made to do than say the host and coreclr properties since those are already "sealed" at this point in time.

Copy link
Member

Choose a reason for hiding this comment

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

Are there any properties that we might want to omit from here?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Here's the list for a very basic .NET 8 console app:

  • FX_DEPS_FILE
  • TRUSTED_PLATFORM_ASSEMBLIES
  • NATIVE_DLL_SEARCH_DIRECTORIES
  • PLATFORM_RESOURCE_ROOTS
  • APP_CONTEXT_BASE_DIRECTORY
  • APP_CONTEXT_DEPS_FILES
  • PROBING_DIRECTORIES
  • RUNTIME_IDENTIFIER
  • System.Reflection.Metadata.MetadataUpdater.IsSupported
  • HOST_RUNTIME_CONTRACT
  • STARTUP_HOOKS

Other identifiers are added to this list when the app contains them in the runtimeOptions:configProperties section in the *.runtimeconfig.json file.

Probably HOST_RUNTIME_CONTRACT can be skipped because its value is a in-memory address to the host runtime contract. Everything else seems to be directory and file paths of various types.

My default position would be to only filter out HOST_RUNTIME_CONTRACT and leave everything else in in the list for reading values. In a future change that may allow modification of values, I can see some more of these being effectively made read-only, but I would have to defer to others on what's desirable to allowed to be read.

@vitek-karas and @elinor-fung, do you have suggestions on what to filter (if any and where to do it) for this initial PR that enables reading the app config values?

Copy link
Member

Choose a reason for hiding this comment

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

The total size of all these switches can be in 10's kB. I assume that this event is going to be typically enabled by default. Do you really want to send 10's kB over the diagnostic channel during startup by default?

I would ask a different question. What do you expect that this data is going to be used for?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The total size of all these switches can be in 10's kB. I assume that this event is going to be typically enabled by default. Do you really want to send 10's kB over the diagnostic channel during startup by default?

There's already an existing diagnostic command for collecting the entire environment block of the target process. I would hazard a guess that it's has similar size characteristics. We haven't seen any significant slowdown during startup nor have we received any customer feedback about it.

The only one that I see right now that is significantly large is TRUSTED_PLATFORM_ASSEMBLIES, which we could probably do without. Maybe we can categorize the data and have those categories as part of the request payload on the diagnostic command in order to limit the response to the interested data.

I would ask a different question. What do you expect that this data is going to be used for?

Originally, we only wanted to get startup hook information, and append our own startup hook to the list which the process is held up during the diagnostics suspension (which occurs just before managed code execution is started); this request is #83756. It was suggested that this be generalized to get / set app context properties. Expanding it to this general concept also allows us to:

  • read RUNTIME_IDENTIFIER to determine what type of ICorProfilerCallback implementation we should load in the target process, especially differentiating on glibc vs musl-libc implementations.

The following are hypothetical but likely candidates for use:

  • read System.StartupHookProvider.IsSupported and determine if we should bother with the startup hook at all. This would also allow us to automatically tell users if features that depend on the startup hook will work. Other app context switches may be useful; not sure how to determine the full set of "in box" switches though.
  • read BUNDLE_PROBE \ HOSTPOLICY_EMBEDDED \ PINVOKE_OVERRIDE to check if the process is single file; some diagnostic scenarios do not work in single file applications. It would be good to know whether we bother trying to set up for those scenarios or be able to give the user a better indication why the diagnostic scenario didn't work (e.g. "sorry, this feature is not compatible with single file applications").
  • read APP_CONTEXT_BASE_DIRECTORY and tell our profiler what that is; this may help in discovering application symbol information that was published with the application so that we can report that back to the .NET Monitor process in order to get SourceLink information. We can use that information in combination with diagnostic artifacts that are collected to help guide users or automated tooling back to real source code.
  • (this is a stretch) read / set APP_PATHS / PROBING_DIRECTORIES to get our managed assemblies loadable without having to rely on managed assembly resolving and LoadFrom context.

Those are the obvious ones that I can think of right now; I can probably dig out more scenarios where some of this data may be useful.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

For my own reference, some (all?) feature switches seem to be documented here and surfaced in the SDK here.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Re single-file crash dump: Wouldn't it be cleaner if the runtime responded via the IPC channel if the dump could be collected or not? After all, the runtime knows the answer... using hints like single-file is just guessing. I understand that guessing is all we have right now, but if we're introducing new functionality why not design it for the original purpose?

I agree. I'm dropping the "other" bonus usage and won't mention it anymore unless others keep poking at it. I just want startup hooks #83756 and runtime identifiers #74476

Copy link
Member

Choose a reason for hiding this comment

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

Startup hooks only execute at startup... they are never evaluated again after the first processing of the startup hooks at the very beginning of managed execution

We can extend this contract via diagnostic command. The startup hook instantiation is load assembly + invocation of the well-known entrypoint. Load assembly has built-in synchronization that ensures that given assembly is loaded exactly once. And once the entrypoint is executing, it can do its own synchronization if there is a chance that it can be invoked multiple times by the diagnostic command.

Once you have managed hook running in the process, you can do any inspection you want, with any precision you like. For example, you do not have to depend on guessing to figure out what kind of libc is loaded based on the RID. You can inspect the process from inside to see what kind of libc is in use. You do not need to depend on runtime adding built-in tracing for every piece of information that you need.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

For example, you do not have to depend on guessing to figure out what kind of libc is loaded based on the RID.

Determining the libc type during diagnostic suspension is not for startup hook usage; it's for determining which variant of our profiler to use.

For loading the profiler, either AddStartupProfiler and AttachProfiler diagnostic commands are invoked, both of which take a full file path to the profiler implementation. The former command is only usable during the suspension and does not evaluate whether the library is loadable or not because the actual load comes later after the runtime has been resumed. There is no feedback on whether the library is loaded or not, so we can't even just try one and fallback to the other; we need to know the RID before the runtime is resumed. For the latter, we could just try one and then the other if the first fails, but that feels like a waste if we already know what the RID is due to needing it for AddStartupProfiler.

You do not need to depend on runtime adding built-in tracing for every piece of information that you need.

I have never asked for that. There is no tracing involved.

To make decisions on what needs to be loaded or enable at diagnostic suspension, we need diagnostic commands to get specific information (such as RID) because some features (such as profilers) require native-specific information and managed code is not executing at that time thus cannot help in a platform agnostic way.

Copy link
Member

@jkotas jkotas May 23, 2023

Choose a reason for hiding this comment

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

we need diagnostic commands to get specific information (such as RID) because some features (such as profilers)

RID is not appropriate for this need. For example, what are you going to do when you see RID void-x64?

If you need libc type to load the right profiler flavor, we should introduce diagnostic command that returns the libc type.

Alternatively, you can find out the libc that the process has loaded using /proc file system. Is that an option?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

If you need libc type to load the right profiler flavor, we should introduce diagnostic command that returns the libc type.

I logged #74476 specifically for this a 9 months ago. I'll try to get to this if the diagnostics team cannot.

Alternatively, you can find out the libc that the process has loaded using /proc file system. Is that an option?

No, .NET Monitor is typically not running in the same process namespace in its primary scenario (multi-container environments such as Kubernetes, where .NET Monitor is typically running as a sidecar container and maybe be running as a different user). We can however use that as a fallback mechanism of almost last resort if it happens to be running in the same namespace, which we may do for runtime versions lower than .NET 8.

dn_vector_ptr_t * props_array = static_cast<dn_vector_ptr_t *>(context);

const LPCWSTR separator = W("=");
size_t name_len = u16_strlen(name);
size_t name_size = name_len * sizeof (ep_char16_t); // no null terminator
size_t separator_len = u16_strlen(separator);
size_t separator_size = separator_len * sizeof (ep_char16_t); // no null terminator
size_t value_len = u16_strlen(value);
size_t value_size = value_len * sizeof (ep_char16_t); // no null terminator

// <name>=<value>\0
size_t str_size = name_size + separator_size + value_size + sizeof (ep_char16_t); // includes null terminator
ep_char16_t *str_entry = reinterpret_cast<ep_char16_t *>(malloc (str_size));
if (!str_entry)
return E_OUTOFMEMORY;

ep_char16_t * dst = str_entry;
memcpy(dst, reinterpret_cast<const ep_char16_t *>(name), name_size);
dst += name_len;
memcpy(dst, reinterpret_cast<const ep_char16_t *>(separator), separator_size);
dst += separator_len;
memcpy(dst, reinterpret_cast<const ep_char16_t *>(value), value_size);
dst += value_len;
dst[0] = (ep_char16_t)'\0';

dn_vector_ptr_push_back (props_array, str_entry);

return S_OK;
}, props_array);
}

/*
* DiagnosticServer.
*/
Expand Down
8 changes: 8 additions & 0 deletions src/mono/mono/eventpipe/ds-rt-mono.h
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,14 @@ ds_rt_disable_perfmap (void)
return DS_IPC_E_NOTSUPPORTED;
}

static
uint32_t
ds_rt_appcontext_properties_get (dn_vector_ptr_t *props_array)
{
// TODO: Implement.
return DS_IPC_E_NOTSUPPORTED;
}

/*
* DiagnosticServer.
*/
Expand Down
186 changes: 186 additions & 0 deletions src/native/eventpipe/ds-process-protocol.c
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,12 @@ process_protocol_helper_disable_perfmap (
DiagnosticsIpcMessage *message,
DiagnosticsIpcStream *stream);

static
bool
process_protocol_helper_get_appcontext_properties (
DiagnosticsIpcMessage *message,
DiagnosticsIpcStream *stream);

static
bool
process_protocol_helper_unknown_command (
Expand Down Expand Up @@ -475,6 +481,131 @@ ds_env_info_payload_fini (DiagnosticsEnvironmentInfoPayload *payload)
payload->env_array = NULL;
}

/*
* DiagnosticsAppContextPropertiesPayload.
*/

static
uint16_t
ds_appcontext_properties_payload_get_size (DiagnosticsAppContextPropertiesPayload *payload)
{
EP_ASSERT (payload != NULL);

size_t size = 0;
size += sizeof (payload->incoming_bytes);
size += sizeof (payload->future);

EP_ASSERT (size <= UINT16_MAX);
return (uint16_t)size;
}

static
uint32_t
ds_appcontext_properties_get_size (DiagnosticsAppContextPropertiesPayload *payload)
{
EP_ASSERT (payload != NULL);

size_t size = 0;

size += sizeof (uint32_t);
size += (sizeof (uint32_t) * dn_vector_ptr_size (payload->props_array));

DN_VECTOR_PTR_FOREACH_BEGIN (ep_char16_t *, prop_value, payload->props_array) {
size += ((ep_rt_utf16_string_len (prop_value) + 1) * sizeof (ep_char16_t));
} DN_VECTOR_PTR_FOREACH_END;

EP_ASSERT (size <= UINT32_MAX);
return (uint32_t)size;
}

static
bool
ds_appcontext_properties_payload_flatten (
void *payload,
uint8_t **buffer,
uint16_t *size)
{
DiagnosticsAppContextPropertiesPayload *props_payload = (DiagnosticsAppContextPropertiesPayload*)payload;

EP_ASSERT (payload != NULL);
EP_ASSERT (buffer != NULL);
EP_ASSERT (*buffer != NULL);
EP_ASSERT (size != NULL);
EP_ASSERT (ds_appcontext_properties_payload_get_size (props_payload) == *size);

// see IPC spec @ https://github.com/dotnet/diagnostics/blob/main/documentation/design-docs/ipc-protocol.md
// for definition of serialization format

bool success = true;

// uint32_t incoming_bytes;
memcpy (*buffer, &props_payload->incoming_bytes, sizeof (props_payload->incoming_bytes));
*buffer += sizeof (props_payload->incoming_bytes);
*size -= sizeof (props_payload->incoming_bytes);

// uint16_t future;
memcpy(*buffer, &props_payload->future, sizeof (props_payload->future));
*buffer += sizeof (props_payload->future);
*size -= sizeof (props_payload->future);

// Assert we've used the whole buffer we were given
EP_ASSERT(*size == 0);

return success;
}

static
bool
ds_appcontext_properties_stream_properties (
DiagnosticsAppContextPropertiesPayload *payload,
DiagnosticsIpcStream *stream)
{
DiagnosticsAppContextPropertiesPayload *props_payload = (DiagnosticsAppContextPropertiesPayload*)payload;

EP_ASSERT (payload != NULL);
EP_ASSERT (stream != NULL);

// see IPC spec @ https://github.com/dotnet/diagnostics/blob/main/documentation/design-docs/ipc-protocol.md
// for definition of serialization format

bool success = true;
uint32_t bytes_written = 0;

// Array<Array<WCHAR>>
uint32_t props_len = dn_vector_ptr_size (props_payload->props_array);
props_len = ep_rt_val_uint32_t (props_len);
success &= ds_ipc_stream_write (stream, (const uint8_t *)&props_len, sizeof (props_len), &bytes_written, EP_INFINITE_WAIT);

DN_VECTOR_PTR_FOREACH_BEGIN (ep_char16_t *, prop_value, props_payload->props_array) {
success &= ds_ipc_message_try_write_string_utf16_t_to_stream (stream, prop_value);
} DN_VECTOR_PTR_FOREACH_END;

return success;
}

DiagnosticsAppContextPropertiesPayload *
ds_appcontext_properties_payload_init (DiagnosticsAppContextPropertiesPayload *payload, const dn_vector_ptr_t *props_array)
{
ep_return_null_if_nok (payload != NULL);
ep_return_null_if_nok (props_array != NULL);

payload->props_array = props_array;
payload->incoming_bytes = ds_appcontext_properties_get_size (payload);
payload->future = 0;

return payload;
}

void
ds_appcontext_properties_payload_fini (DiagnosticsAppContextPropertiesPayload *payload)
{
DN_VECTOR_PTR_FOREACH_BEGIN (ep_char16_t *, prop_value, payload->props_array) {
ep_rt_utf16_string_free (prop_value);
} DN_VECTOR_PTR_FOREACH_END;

payload->props_array = NULL;
}

/*
* DiagnosticsProcessProtocolHelper.
*/
Expand Down Expand Up @@ -872,6 +1003,58 @@ process_protocol_helper_disable_perfmap (
ep_exit_error_handler ();
}

static
bool
process_protocol_helper_get_appcontext_properties (
DiagnosticsIpcMessage *message,
DiagnosticsIpcStream *stream)
{
EP_ASSERT (message != NULL);
EP_ASSERT (stream != NULL);

bool result = false;

DiagnosticsAppContextPropertiesPayload payload;
DiagnosticsAppContextPropertiesPayload *props_payload = NULL;

dn_vector_ptr_t *props_array = dn_vector_ptr_alloc ();
ep_raise_error_if_nok (props_array);

ds_ipc_result_t ipc_result;
ipc_result = ds_rt_appcontext_properties_get (props_array);
if (ipc_result != DS_IPC_S_OK) {
ds_ipc_message_send_error (stream, ipc_result);
ep_raise_error ();
} else {
props_payload = ds_appcontext_properties_payload_init (&payload, props_array);
ep_raise_error_if_nok (props_payload);

ep_raise_error_if_nok (ds_ipc_message_initialize_buffer (
message,
ds_ipc_header_get_generic_success (),
(void *)props_payload,
ds_appcontext_properties_payload_get_size (props_payload),
ds_appcontext_properties_payload_flatten));

ep_raise_error_if_nok (ds_ipc_message_send (message, stream));
ep_raise_error_if_nok (ds_appcontext_properties_stream_properties (props_payload, stream));
}

result = true;

ep_on_exit:
ds_appcontext_properties_payload_fini (props_payload);
dn_vector_ptr_free (props_array);
ds_ipc_stream_free (stream);
return result;

ep_on_error:
EP_ASSERT (!result);
ds_ipc_message_send_error (stream, DS_IPC_E_FAIL);
DS_LOG_WARNING_0 ("Failed to send DiagnosticsIPC response");
ep_exit_error_handler ();
}

static
bool
process_protocol_helper_unknown_command (
Expand Down Expand Up @@ -916,6 +1099,9 @@ ds_process_protocol_helper_handle_ipc_message (
case DS_PROCESS_COMMANDID_DISABLE_PERFMAP:
result = process_protocol_helper_disable_perfmap (message, stream);
break;
case DS_PROCESS_COMMANDID_GET_APPCONTEXT_PROPERTIES:
result = process_protocol_helper_get_appcontext_properties (message, stream);
break;
default:
result = process_protocol_helper_unknown_command (message, stream);
break;
Expand Down
29 changes: 29 additions & 0 deletions src/native/eventpipe/ds-process-protocol.h
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,35 @@ ds_enable_perfmap_payload_alloc (void);
void
ds_enable_perfmap_payload_free (DiagnosticsEnablePerfmapPayload *payload);

/*
* DiagnosticsAppContextPropertiesPayload
*/

#if defined(DS_INLINE_GETTER_SETTER) || defined(DS_IMPL_PROCESS_PROTOCOL_GETTER_SETTER)
struct _DiagnosticsAppContextPropertiesPayload {
#else
struct _DiagnosticsAppContextPropertiesPayload_Internal {
#endif
// The appcontext properties are sent back as an optional continuation stream of data.
// It is encoded in the typical length-prefixed array format as defined in
// the Diagnostics IPC Spec: https://github.com/dotnet/diagnostics/blob/main/documentation/design-docs/ipc-protocol.md
uint32_t incoming_bytes;
uint16_t future;
const dn_vector_ptr_t *props_array;
};

#if !defined(DS_INLINE_GETTER_SETTER) && !defined(DS_IMPL_PROCESS_PROTOCOL_GETTER_SETTER)
struct _DiagnosticsAppContextPropertiesPayload {
uint8_t _internal [sizeof (struct _DiagnosticsAppContextPropertiesPayload_Internal)];
};
#endif

DiagnosticsAppContextPropertiesPayload *
ds_appcontext_properties_payload_init (DiagnosticsAppContextPropertiesPayload *payload, const dn_vector_ptr_t *props_array);

void
ds_appcontext_properties_payload_fini (DiagnosticsAppContextPropertiesPayload *payload);

/*
* DiagnosticsProcessProtocolHelper.
*/
Expand Down
4 changes: 4 additions & 0 deletions src/native/eventpipe/ds-rt.h
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,10 @@ static
uint32_t
ds_rt_disable_perfmap (void);

static
uint32_t
ds_rt_appcontext_properties_get (dn_vector_ptr_t *props_array);

/*
* DiagnosticServer.
*/
Expand Down
Loading