Worst API ever created

Original author: Casey Muratori
  • Transfer

A detailed look at logging context switching using the Event Tracing API for Windows.


In response to last week's post, I received the following email:
Maybe I was a little late with the question about your recent post, but just in case, I’ll ask: Do you have any methods (strategies) for working with an external library that you cannot get rid of, and which violates some (or all) ) the principles of writing the design of the API (most likely, it refers to the principles, recommendations, which are described in the previous article of the author. Note transl. )? Maybe there are some stories? This is a vague question, but I'm just asking about any experience (as a user) using the API that I really remember.

- Michael Bartnett

This reminded me of the fact that, in fact, I always wanted to describe in detail the steps necessary to use a bad API - just to emphasize how terrible it can be for a developer. I don’t think that people who develop the API really understand how important it is to do it right and how much unnecessary work hundreds, thousands, and sometimes millions of other developers do when the API is incorrectly developed incorrectly. So, I felt that it is important enough to write an article that will show how much unnecessary work the written API can cause.

This would probably be a good article in the weekly parsing cycle of bad APIs. But, since I don’t have time for something like that and I can only parse one API, the most important question arises - which API should I choose?

Event Tracing API for Windows




Now is a great time in the history of computer technology for writing an article about a bad API (on the other hand, we can say that this is the worst time to make a living with programming). There are so many bad APIs now that I can randomly select one API and most likely find enough problems to write a 3,000-word article. But if I was going to select one, separate operation in one API, then my choice seems reasonable among all the APIs I have ever used.

Currently, there are a lot of APIs that are making a lot of efforts to get into the top “worst APIs” rating. For example, CSS, when a new version appears, may take up half of the places in the top 10 ranking for a year. During its popularity, DirectShow certainly dominated the ranking of its era. In the new generation, newcomers such as the Android SDK, together with development tools, demonstrate real potential in their complexity, so the quality of the API when called from C ++ code is the last thing you are worried about.

But when I thought for a long time and hard about who the winner was in the “heavy category of bad API” - there was one real thing - Event Tracing API for Windows.

The Event Tracing API for Windows is an API that does something very simple: it allows any component of the system (including regular applications) to notify of “events” that can be received (“absorbed”) by any other component. This is a logging system that is used to record the performance and debugging information of any component, starting from the core of the system.

Now, usually for game developers, there is no reason to use the Event Tracing API for Windows directly. You can use utilities such as PerfMon to view information about your game, such as how many working sets it uses or how intensively it works with the disk. But there is one specific thing that only direct access to the Event Tracing API provides: the ability to track context switching time.

Yes, if you have a fairly recent version of Windows (for example, 7 or 8), the OS kernel will log all context switches, including the CPU timestamp. You, in fact, can correlate them with your own profiling in the game. This is incredibly useful information (from the category of information that can only be obtained directly from the "iron"). This is the reason why utilities like RAD , Telemetry can show you when the threads you started were interrupted and should wait until the threads of the system do their work; something that can be critical for debugging weird performance issues.

That sounds pretty good. I mean, context switching time is very important information, and even if the API is not of the best quality, it is, nevertheless, very cool, isn't it?

Is not it?

First of all - write an example of using the API



Before we take a look at the real Event Tracing API for Windows, I want to step by step do what I talked about in a lecture last week: first write an example of use. Whenever you evaluate the quality of an API, or create a new API, you always, always, ALWAYS start by writing some code as if you are a user who is trying to do the things that your API is intended for. If there are no restrictions, this is the only way to get a good and clean look at the future of how the API will work. That would be "magical." And then, when you have some examples of use, you can move forward and start thinking about practical problems and about the best implementation method for you.

So, if I were a developer without any knowledge of the Event Tracing API for Windows, how would I like to get a list of context switches? Well, 2 ways come to mind.

The simplest approach would be like:

// В начале программы
etw_event_trace Trace = ETWBeginTrace();
ETWAddEventType(Trace, ETWType_ContextSwitch);
// Для каждого кадра
event EventBuffer[4096];
int EventCount;
while(EventCount = ETWGetEvents(Trace, sizeof(EventBuffer), EventBuffer))
{
    {for(int EventIndex = 0;
        EventIndex < EventCount;
        ++EventIndex)
    {
        assert(EventBuffer[EventIndex].Type == ETWType_ContextSwitch);
        // обработать EventBuffer[EventIndex].ContextSwitch
    }}
}
// В конце программы
ETWEndTrace(Trace);

which leads to an API that looks like this:

enum etw_event_type
{
    ETWType_None,
    ETWType_ContextSwitch,
    ...
    ETWType_Count,
};
struct etw_event_context_switch
{
    int64_t TimeStamp;
    uint32_t ProcessID;
    uint32_t FromThreadID;
    uint32_t ToThreadID;
};
struct etw_event
{
    uint32_t Type; // event_type
    union
    {
        etw_event_context_switch ContextSwitch;
        ...
    };
};
struct etw_event_trace
{
    void *Internal;
};
event_trace ETWBeginTrace(void);
void ETWAddEventType(event_trace Trace, event_type);
int ETWGetEvents(event_trace Trace, size_t BufferSize, void *Buffer);
void ETWEndTrace(event_trace Trace);


This is one way to do it. Very simple, basic to understand. It’s hard enough to make a mistake. If someone went through the debugger, you would see exactly what is happening and you could simply say what went wrong.

However, I can imagine a situation where performance-critical code would not want to pay for copying from the kernel buffer to your buffer - what this API requires (it ETWGetEventsshould copy events from some internal OS buffer, since they need to be taken from somewhere). The slightly more complicated version will take some mapped memory using the API, which you use as a buffer for reading:

// В начале программы
etw_event_trace Trace = ETWBeginTrace(4096*sizeof(etw_event));
ETWAddEventType(Trace, ETWType_ContextSwitch);
// Для каждого кадра
etw_event_range Range;
while(ETWBeginEventRead(Trace, &Range))
{
    {for(etw_event *Event = Range.First;
        Event != Range.OnePastLast;
        ++Event)
    {
        assert(Event->Type == ETWType_ContextSwitch);
        // обработать Event->ContextSwitch
    }}
    ETWEndEventRead(Trace, &Range);
}
// В конце программы
ETWEndTrace(Trace);


All I did here was change the return mechanism: instead of copying, a pointer to a certain block (“ranged pointer” - a pointer to a certain range. In general, a pointer that knows where the data it points to ends. Note perev . ). B ETWBeginTrace, the user transmits the maximum number of events, which affects the size of the buffer and the kernel allocates a memory area (in the user's address space), sufficient for the specified number of events. Then the system, if it can, writes directly to the dedicated buffer and thereby avoids unnecessary copying. When the user callsETWBeginEventRead(), pointers are returned to the beginning and end of some memory for events. Since the buffer will be processed as a circular buffer, the user expects to be able to cycle through all the received events when there are more than one events. I added the “end of reading” call, as some implementations may require the kernel to know what part of the buffer the user is viewing, which will avoid writing to memory that is actively being read. I really don’t know if such things are needed at all, but if you want to get basic information and give the kernel maximum flexibility for implementation, this version certainly supports more possible implementations than version c ETWGetEvents().

The API will be updated, for example, like this:

struct etw_event_range
{
    etw_event *First;
    etw_event *OnePastLast;
};
event_trace ETWBeginTrace(size_t BufferSize);
int ETWBeginEventRead(event_trace Trace, etw_event_range *Range);
void ETWEndEventRead(event_trace Trace, etw_event_range *Range);


If you really want to, you can even support both versions of reading events using the same API - you just need to allow the call ETWGetEvents(). Also, to supplement the API with error messages, it would be nice to have something like this:

bool ETWLastGetEventsOverflowed(event_trace Trace);

so that after each call to ETWGetEvents()be able to check if too many events have occurred since the last check?

To each his own, but I think that most developers will not have any problems with the API that I just proposed. Each one has its own tastes and I'm sure that everyone will notice something that he does not like, but I doubt that someone will say that the API is terrible. The API is pretty simple and I think that most developers will be able to easily implement this API in their code without too much thought.

The reason the API is so simple is not because I used a lot of experience developing the API to subtly show my vision of a good API. On the contrary. The API is simple because the problem it is intended to solve is elementary. How to move data from one place to another is, in fact, the simplest API problem that can occur on a system. It is glorified memcpy().

But it is the simplicity of the task that allows the Event Tracing API for Windows to shine. Even if all you need to do is move the memory from one place to another, using this API there are all kinds of complexities that you can imagine.

"Launch" tracking



I don’t know how anyone wants to start learning how to use the Event Tracing API for Windows. Maybe there are good examples teaching this that I just never met. I had to put together pieces of code taken from various snippets of documentation for many hours of experimentation. Every time I figured out one more step in the overall process, I thought: “Wait, really ??”. And every time Microsoft implicitly answered: "Seriously."

If I tell you how to use the API, you will lose the opportunity to experience a soulful experience, so I’ll say that if you want to feel everything in full, stop reading and try to get the context switching time yourself. I can guarantee that you will receive hours of unbridled fun and anxiety. Those of you who prefer to save time by avoiding a bunch of obscure moments - just read on.

GOOD, get started. The equivalent of my functions ETWBeginTrace()- it is a challenge, the Microsoft predlozhonnoy, StartTrace(). At first glance, it seems pretty harmless:
ULONG StartTrace(TRACEHANDLE *SessionHandle, char const *SessionName,
    EVENT_TRACE_PROPERTIES *Properties);


However, when you look at what needs to be passed in place of a Propertiesparameter, things get a little more complicated. The structure EVENT_TRACE_PROPERTIESdefined by Microsoft looks like this:

struct EVENT_TRACE_PROPERTIES
{
    WNODE_HEADER Wnode;
    ULONG BufferSize;
    ULONG MinimumBuffers;
    ULONG MaximumBuffers;
    ULONG MaximumFileSize;
    ULONG LogFileMode;
    ULONG FlushTimer;
    ULONG EnableFlags;
    LONG AgeLimit;
    ULONG NumberOfBuffers;
    ULONG FreeBuffers;
    ULONG EventsLost;
    ULONG BuffersWritten;
    ULONG LogBuffersLost;
    ULONG RealTimeBuffersLost;
    HANDLE LoggerThreadId;
    ULONG LogFileNameOffset;
    ULONG LoggerNameOffset;
};


где, в свою очередь, первый член данных — это также структура, которая разворачивается в следующее:

struct WNODE_HEADER
{
    ULONG BufferSize;
    ULONG ProviderId;
    union
    {
        ULONG64 HistoricalContext;
        struct
        {
            ULONG Version;
            ULONG Linkage;
        };
    };
    union
    {
        HANDLE KernelHandle;
        LARGE_INTEGER TimeStamp;
    };
    GUID Guid;
    ULONG ClientContext;
    ULONG Flags;
};

Беглый взгляд на эту массу странных данных вызывает только вопросы: почему здесь есть такие члены, как "EventsLost" и "BuffersWritten" («количество не записанных событий» и «количество записанных буферов», соответственно, — из документации. Прим. перев.)? The reason is the following, instead of creating different data structures for different operations that can be used to track events, Microsoft has grouped the API functions into several groups and all the functions in each group share the combined structure for the parameters. Therefore, instead of giving the user a clear idea of ​​what is being input and returning from the function, just looking at the parameters of the function - he should completely depend on the MSDN documentation for each API, and hope that the documentation correctly lists which members of the giant Parameter structures are used for every call and when exactly they are designed to transmit input and output parameters.

Of course, since there are so many different ways to use this structure, Microsoft requires you to completely zero out this huge beast before using it:

EVENT_TRACE_PROPERTIES SessionProperties = {0};

For a function StartTrace(), if you just want to receive the data directly and will not log them into a file, you need to fill in some members. These two have some meaning:

SessionProperties.EnableFlags = EVENT_TRACE_FLAG_CSWITCH;
SessionProperties.LogFileMode = EVENT_TRACE_REAL_TIME_MODE;

Member EnableFlagssays we want to receive. We want to switch contexts, therefore we set this flag. Now let's see what happens when there are more than 32 events from one provider? I do not know, but I suppose that Microsoft was not particularly concerned about this possibility. But I was, that's why I used it enumin my proposal, since it supports billions of event types. But wait, “32 event types should be enough for everyone” - that's why Microsoft proposed a 32-bit flag field. There is no problem, but this is definitely a sign of Exnarrow-minded thinking, which leads to things like duplication of functions with a prefix at the end of the name (“Ex” - from “Extended” - an expanded version of the function. Note translation ).

LogFileModedetermines whether we want to receive events directly or if we want the kernel to write them to disk. Since these are completely different operations, I would like to break them down into calls to different functions, but hey, we have one big structure for everything - we drop everything there.

With this field, things get a little weird:

SessionProperties.Wnode.ClientContext = 1;

What does this entry mean? Well, the horribly named "ClientContext"(“Client Context.” Approx. Transl. ), Actually refers to the form in which you want to get the time of events. "TimestampType"(“Type of TimeTags.” Approx. Transl. ) Would be a little clearer, but not important. The real fun is the simple one "1"on the right.

It turns out that there is a set of values ​​that ClientContextcan take, but Microsoft does not always give them names. Thus, you just have to read the documentation and remember that 1 means that the time comes from QueryPerformanceCounter, 2 means "system time", and 3 means the number of CPU cycles.

In case this is not obvious, there are many reasons why a publicly accessible API should never do this. In the hidden part of the implementation, from time to time, I will do this in situations where, say, you need to use -1 and -2 for some kind of intricate indexing scheme. But for the APIs that literally millions of developers use, you should always give names to your constants.

Firstly, it makes the code more readable. No one knows what the ClientContextmeaning is "1", but the meaning USE_QUERY_PERFORMANCE_COUNTER_TIMESTAMPSwill be crystal clear. Secondly, it makes the code searchable. No one will be able to normally search for "1" in the code base, but it USE_QUERY_PERFORMANCE_COUNTER_TIMESTAMPSis easily searched. You might think: “well, no problem, I’ll lookClientContext = 1"But remember that the more sophisticated use of the API may include the use of variables, for example: ". . .ClientContext = TimestampType;". Third, the code will not compile for future versions of the SDK, where some things have changed. For example, if developers decide to prohibit use USE_QUERY_PERFORMANCE_COUNTER_TIMESTAMPS, they can remove the definition ( #define) of this constant and make it USE_QUERY_PERFORMANCE_COUNTER_TIMESTAMPS_DEPRECATED. After such changes, the old code will not compile with the new version of the SDK and the developer will look at the new documentation and thus see what he should use in return.

Etc., etc., etc.

Perhaps the most annoying field we need to fill out:

SessionProperties.Wnode.Guid = SystemTraceControlGuid;


GUIDtalks about who is trying to track events. In our case, we are trying to take data from the kernel log and SystemTraceControlGuidis globally defined GUID, which points to this particular log. I’m sure that GUIDone could give a better name for this , but this is an insignificant problem compared to the fact that if you try to collect this line of code, you will see that the linker cannot be found SystemTraceControlGuid.

This happens, of course, because it's GUIDso big that Microsoft may not have been able to find a way to embed them in the header files (I can count several possible ways, but I suppose they didn't like any of them), so Microsoft forces you to select one file in your project into which, the Windows header files will embed the definitionGUIDs. In order to do this, you should write something like this:

#define INITGUID  // Заставляет определить SystemTraceControlGuid внутри evntrace.h.
#include 
#include 
#include 
#include 
#include 

So, now you have to carefully choose where you will do this - perhaps create a new file in your project, where everyone will be located GUID- everyone will be able to refer to them (or some other nonsense there). In general, so that you cannot identify them twice.

But, be that as it may, we are almost done filling out the structure. All we need to do is figure out the parameter SessionNamethat we need to pass as a string, right? Since this is just the name of the session, I assume I can just do the following:

ULONG Status = StartTrace(&SessionHandle,
    "ОтладчикCaseyTownУДИВИТЕЛЬНЫЙДА!!!", &SessionProperties);

because this is the cool name for the session, don't you think so?

But alas, things do not work that way. It turns out that despite the fact that you have already indicated GUIDfor SessionProperties, which says that the kernel is the source of events, you also need to specify a predefined constant KERNEL_LOGGER_NAMEas the name of the session. Why? Well, this is because there is a little surprise that I will keep for you so that you can enjoy the intrigue of everything that happens.

Ok so start:

ULONG Status = StartTrace(&SessionHandle, KERNEL_LOGGER_NAME,
    &SessionProperties);

It looks good, right? WRONG.

It turns out that, despite the fact that the string is SessionNamepassed in as the second parameter, this is just a “convenient” feature. In fact, it SessionNameshould be implemented right in SessionProperties, but since Microsoft does not want to limit the maximum length of the name string, it was decided to just go ahead and pack this string after the structure EVENT_TRACE_PROPERTIES. Which means that in fact, you can’t do this:

EVENT_TRACE_PROPERTIES SessionProperties = {0};

And they should do this:

ULONG BufferSize = sizeof(EVENT_TRACE_PROPERTIES) + sizeof(KERNEL_LOGGER_NAME);
EVENT_TRACE_PROPERTIES *SessionProperties =(EVENT_TRACE_PROPERTIES*) malloc(BufferSize);
ZeroMemory(SessionProperties, BufferSize);
SessionProperties->LoggerNameOffset = sizeof(EVENT_TRACE_PROPERTIES);


Yes, everything is correct - each user of the Event Tracing API for Windows must do arithmetic calculations and manually arrange the structure in a packed format. I have no idea why the name should be packed in this way, but I’m sure that if you need everyone to do this, you must provide an auxiliary macro or function that will do the right thing for the user and save him from understanding your strange data packaging logic.

But wait, at least you don’t need to copy the name yourself! Microsoft decided that the function of StartTrace()this API will copy the name into the structure for you, because in the end, the name is passed as the second parameter.

Well, this is a beautiful gesture, but it’s not accepted in practice. It turns out that the forced transmissionKERNEL_LOGGER_NAMEin quality SessionName- along with GUID-th - is not, after all, superfluous, and this is the surprise I mentioned. The real reason why the parameter SessionNameshould be set in KERNEL_LOGGER_NAMEis because Windows allows you to have only one session in the system - in general - a session that reads events from SystemTraceControlGuid. Others GUIDmay have several sessions, but not SystemTraceControlGuid. So, in fact, when you transmit KERNEL_LOGGER_NAME- you say that you want to have one, unique, session that can exist in the system at any time with SystemTraceControlGuidGUID-th. If someone has already started this session - your attempt to start it will fail.

Hoh, it’s getting better. The session name is global for the entire operating system and does not close automatically when the process that started this session abnormally terminated. So, if you wrote code that calls StartTrace(), but a bug slipped somewhere - no matter how - and your program crashed - the KERNEL_LOGGER_NAMEsession will still work! And, when you try to run your program again, perhaps after fixing the bug, then your attempt to call StartTrace()will fail ERROR_ALREADY_EXISTS.

So, in principle, a call StartTrace()that helpfully copies SessionNameto the structure for you is, in rare cases, the first call you want to make, anyway. Most likely you will make the following call:

ControlTrace(0, KERNEL_LOGGER_NAME, SessionProperties, EVENT_TRACE_CONTROL_STOP);

This call ends any existing session and the subsequent call StartTrace()will succeed. But, of course, it ControlTrace()does not copy the session name in the way it does StartTrace(), which means that, in practice, you must do it yourself, since the call StartTrace()comes after the call ControlTrace()!

StringCbCopy((LPSTR)((char*)pSessionProperties + pSessionProperties->LoggerNameOffset),
    sizeof(KERNEL_LOGGER_NAME), KERNEL_LOGGER_NAME);


This is crazy, but the consequences of all this are even crazier. If you think about what it means to have only one possible tracking process that is connected to the kernel log, you will quickly realize that security issues come into play. But what if some other process called StartTrace()for tracking the kernel log - how does the system know that our process has the ability to take and stop that log tracking and start another with our settings already?

The answer is ridiculous - no way. In fact, tracking the kernel log is completely accessible to everyone - let the best process win! Whoever calls StartTrace(), well, he gets the opportunity to set up log tracking for himself.

Well, not quite. Obviously, you do not want any process to steal kernel log tracking from another process. Therefore, Microsoft decided that the best solution would be to simply deny access to the kernel log to all processes except those that have administrator rights.

Yes, exactly so - I am not exaggerating. If you just want to get a list of context switches, even if for your own process, then it should be started with administrator privileges. All the delights of "right-click-run-as-administrator". If you didn’t do this and just started your process in the usual way, then your callStartTracewill fail, because the process has insufficient privileges. (In theory, you have the opportunity to add a user to the “Performance Log Users” group and thus avoid starting the process as an administrator, but I just say it now - in fact, I can’t remember whether this will work for connections to kernel log or only for all other types of tracking ...)

Surprisingly, yes? In order to do what should be equivalent to calling 2 ordinary functions ( ETWBeginTrace()/ ETWAddEventType()), we did memory counting, memory allocation, freeing up memory, calculating indentation, copying lines, filling out structures, using not one, but two styles for global- GUIDconstants specially used #definebefore#include-processor directives, and require the user to start our process with full administrator rights.

All this, and we have not even received our events!

"Opening" tracking



I know what you think. You think that after the section “Launching tracking” the collection of events from the log should follow, right? Nonsense! People, that's what Event Tracing for Windows is all about. Running tracking does not start tracking! It only half starts tracking! If you want to start tracking for real, then everyone knows that you first start it and then open it ... using the function OpenTrace():

TRACEHANDLE OpenTrace(EVENT_TRACE_LOGFILE *Logfile);

What does this function do? Well, it turns out that the “running” tracking is just a tracking that collects events. It does not, in fact, provide any way to receive these events. If you want to receive them, then you must open the tracking using OpenTrace().

So, in order to cause OpenTrace(), we need EVENT_TRACE_LOGFILE. Of course, in fact, we do not make a log file, we just want to take events and the fact that we are filling something with the name “LOGFILES” is a bit strange. But just as for StartTrace(), OpenTrace()it’s all part of a family of functions that use the same structure parameters together, and, in practice, the fact that the name is inappropriate for our purposes is the smallest nuisance.

The structure is EVENT_TRACE_LOGFILEas follows:

struct EVENT_TRACE_LOGFILE
{
    LPTSTR LogFileName;
    LPTSTR LoggerName;
    LONGLONG CurrentTime;
    ULONG BuffersRead;
    union
    {
        ULONG LogFileMode;
        ULONG ProcessTraceMode;
    };
    EVENT_TRACE CurrentEvent;
    TRACE_LOGFILE_HEADER LogfileHeader;
    PEVENT_TRACE_BUFFER_CALLBACK BufferCallback;
    ULONG BufferSize;
    ULONG Filled;
    ULONG EventsLost;
    union
    {
        EVENT_CALLBACK *EventCallback;
        EVENT_RECORD_CALLBACK *EventRecordCallback;
    };
    ULONG IsKernelTrace;
    PVOID Context;
};

If you became nervous when you saw the word " Callback" (callback function. Approx. Transl. ) - I was also nervous with you. Taking events should be common - just retrieve them from memory. There should not be a need for a callback function here.

But, let's move on, EVENT_TRACE_LOGFILEone of those gigantic variety of collections-structures and Microsoft asks you to first reset it:

EVENT_TRACE_LOGFILE LogFile = {0};

Since the function OpenTrace()does not accept any handles, we must somehow give it a way to find the tracking that we “started” before. It turns out that this is done by normal string matching, so we pass the session name again:

LogFile.LoggerName = KERNEL_LOGGER_NAME;

Strange, but this time we should not copy the line to the end of the structure. Why? Who knows! Diversity is the spice of life, I tell you. Microsoft wants your life to be sharp.

The next step is to fill out the tracking method, which is actually a set of flags:

LogFile.LoggerName = KERNEL_LOGGER_NAME;
LogFile.ProcessTraceMode = (PROCESS_TRACE_MODE_REAL_TIME |
    PROCESS_TRACE_MODE_EVENT_RECORD |
    PROCESS_TRACE_MODE_RAW_TIMESTAMP);

PROCESS_TRACE_MODE_REAL_TIMEas far as I can tell, this is a completely redundant flag, because if you do not specify the name of the log file, then I’m not sure how you can receive the events. PROCESS_TRACE_MODE_EVENT_RECORD- a flag for compatibility, which tells Windows that you want to use the new version EventRecordCallback, and not the old one EventCallback(yes, believe it or not, this awesome API actually went through several revisions!). And the flag PROCESS_TRACE_MODE_RAW_TIMESTAMP- tells Windows not to overwrite the settings ClientContextthat were passed in StartTrace(). I suppose the idea here is this: the person who started the tracking may have used a time format other than " 2" and if you want a " 2" you can always have a " 2" when you receive events. If you like " 1" or " 3"

Finally, we need to tell the API our function that will collect events:

LogFile.EventRecordCallback = CaseyEventRecordCallback;

And then we are finally ready for the big challenge:

TRACEHANDLE ConsumerHandle = OpenTrace(&LogFile);


Before we move on to actually receiving real events (God forbid!), I want you to stop for a moment and admire the real splendor StartTrace()and OpenTrace()features. These are 2 APIs that are part of the same system. They both generate a new one TRACEHANDLE. And each of them may fail. They both accept the name of the session. But they work completely differently. Absolutely!

The function StartTrace()returns a type error code ULONGand takes a pointer to where the return value can be written. The function OpenTrace()directly returns the result of the work, but sets it to INVALID_HANDLE_VALUEif any error occurred.StartTrace()accepts the name of the session as a parameter and forces you to allocate memory (along with the structure parameter) in order to copy the passed string later. OpenTrace()accepts a pointer to the session name and parameter structure, but does not require to copy the transmitted string anywhere.

Event Handling



We worked so hard and went so far - it would be nice to finally get context switching events, wouldn't it? To get them, we, of course, must implement the callback function, which we passed to OpenTrace():
static void WINAPI
CaseyEventRecordCallback(EVENT_RECORD *EventRecord)
{
    EVENT_HEADER &Header = EventRecord->EventHeader;
    UCHAR ProcessorNumber = EventRecord->BufferContext.ProcessorNumber;
    ULONG ThreadID = Header.ThreadId;
    int64 CycleTime = Header.TimeStamp.QuadPart;
    // Process event here.
}


Fortunately, these are just a few readings from the structure and nothing unusual happens here. Events come, we get data and when we want to do something with them, we do it. But when will this function be called?

Well, Windows will only call it when you try to “process” events that are available from open tracking already using the following API:
ULONG ProcessTrace(TRACEHANDLE *HandleArray, ULONG HandleCount,
    LPFILETIME StartTime, LPFILETIME EndTime);


You can use this function by passing a handle to an already open tracking and Windows will spin all the events that it has on the transferred handle, calling your handler for each event. So we are done? We just call this function every frame of our game (or some other suitable interval) and collect events using our function?

I'm afraid not, my friends, because there is a little highlight that makes the Event Tracing API for Windows more savory and flavorful than its contemporaries: the function ProcessTracenever returns.

Yes, you read everything right. EssenceProcessTrace(), for real-time tracking, it is such that it sends out any events that are now available, and then blocks execution and waits for new events. It blocks execution forever or until tracking is manually closed using CloseTrace(). This means that the only way to really get events and continue your process is to create a completely new thread in order to do nothing, but just hang on ProcessTrace()!

You think I'm joking, but I'm completely serious. First you need to make a stub for the thread that will block forever when called ProcessTrace():
static DWORD WINAPI
Win32TracingThread(LPVOID Parameter)
{
    ProcessTrace(&ConsumerHandle, 1, 0, 0);
    return(0);
}


Then, after you called OpenTrace()- you must start this thread in order to do event processing:
DWORD ThreadID;
HANDLE ThreadHandle = CreateThread(0, 0, Win32TracingThread, 0, 0, &ThreadID);
CloseHandle(ThreadHandle);


This, literally, is the only way I know to get frame by frame events for a running program using the Event Tracing API for Windows.

A look at the API as a whole



So, here it is, ladies and gentlemen: the only API that I have used that requires elevated privileges and a dedicated user in order to just copy a block of memory from the kernel to the user. I have never seen anything like it and will never see it again. Add this to what-not-to-do-songs along with the actual API calls, and I hope everyone agrees that the Event Tracing API for Windows is a completely bad API in itself.

You can critically evaluate this API by looking at the principles that I described last week (okay, 10 years ago). For example, we can say that the required stream and callback function for simple memory transfer is a red flag for the principle of "Flow Control". You can specify that there is no detail at all and you must make function calls exactly as I described. It can be said that almost all data and functions are collected in a dozen different ways, including such oddities as the requirement of what SessionNameshould be KERNEL_LOGGER_NAMEif GUID- SystemTraceControlGuid.

But, in fact, the most important lesson that can be learned from such a bad API as this one is “first of all - write an example of using the API”. At the lecture, I said that this is the first and second rule for the design of the API and I was not joking. Without thinking about the principles and getting bogged down in details - the usual exercise of writing what the API should look like - all that was really needed in order to see all the places where the version from Microsoft had failed. If you look back and compare 2 versions (see the “First of all - write an example of using the API” part. Note. Transl. ), You will immediately see how complicated, error-prone, designed to use the developer’s intuition is the version from Microsoft.

From the translator :
this is my first experience of translating an article - I ask to send all outrageous inaccuracies / typos / nonsense with a personal message (if possible).

Since in life it turns out to develop under Windows, I often hear, when comparing the Windows and Linux APIs, that only the Windows API is bad, and the Linux API is almost perfect - there are no problems. I would like to know your opinion. Are there such shoals in the Linux API world?

Only registered users can participate in the survey. Please come in.

How do you like the Event Tracing API?

  • 66.7% I have never seen such a thing - terrible 189
  • 21.9% A little strange, but, in general, everything is in order 62
  • 11.3% Great API. I do not understand what the author of the original article 32 finds fault with

How do you like the translation?

  • 17.8% Muck. Stop don't do it anymore 49
  • 28.1% OK. I tried - and that's enough 77
  • 54% Good. You can continue to translate 148

Also popular now: