Tuesday, February 11, 2014

Dynamic Strings: Solution to a Problem and a Problematic Solution Combined

Telemetry's run-time markup system relies on the assumption that string identifiers are both constant and (hopefully) string pooled.  This allows us to send over a string only once when we encounter it, e.g.:

void foo( void )
{
   tmZone( cx, TMZF_NONE, "hello world" ); // "hello world" should be sent only the first time we see it

   // do a bunch of foo stuff
}

This works great for the general case where you're statically marking up a lot of code, which is common when using compiler provided constants like __FUNCTION__.

However sometimes the description you pass to tmEnter or tmZone isn't a const string (e.g. it may be copied out of string table, etc.).  Or (more likely) one of the string parameters you're passing to your zone markup isn't actually a const string and instead a string on the stack.

void foo( char const *name )
{
   // if 'name' points to something on the stack, this may be bad...
   tmZone( cx, TMZF_NONE, "foo: %s", name );
   // do a bunch of foo stuff
}

Here's where the problem occurs.  Since Telemetry assumes that the string pointers passed to it are const, it will try to read from name in its background processing thread probably long after name is out of scope and gone.  In addition, once it sees name's address, it won't send it again.  This is to save a lot of bandwidth, but if the contents of name are constantly changing (which is highly likely) you'll see a lot of the same string repeating in the Telemetry Visualizer (since it's never resent with the new content) instead of the updated string.

(Instead of just using the pointer we could hash the contents, but that has a surprising amount of overhead when working with lots of strings, particularly on lower end devices)

Telemetry's solution to this problem is to allow you to tag certain strings as dynamic, i.e. volatile memory that may change or disappear almost immediately.

void foo( char const *name )
{
   tmZone( cx, TMZF_NONE, "foo: %s", tmDynamicString( cx, name ) );
   // do a bunch of foo stuff
}

Now the full contents of name are sent over immediately every time they're encountered.  In exchange for correctness we've now incurred a potentially massive amount of network overhead.  This is fine as a first pass integration (to make sure your markup is working), but if you leave it in and have a lot of markup you may find that Telemetry is suddenly eating a tremendous amount of network bandwidth due to redundant string sends.  This is really a problem on devices with limited bandwidth to begin with such as WiiU, XBOX 360, and mobile.

Figuring all this out can be a headache, so thankfully Telemetry's server automatically generates plots indicating the amount of dynamic string activity in a session.  If you enable the plots you should be able to see if you're hammering the dynamic string system or not.  A few dozen dynamic strings should be fine, but if you're finding that you're sending over hundreds or thousands of strings every frame, that is likely bad.


If you find yourself in this situation then you'll want to invest some time doing a string pooling/interning system that provides a const mapping from a volatile string to a static one.  There's no easy way for Telemetry to provide this functionality since it depends heavily on your app's underlying string management architecture, but it's a worthwhile investment for the performance.

No comments:

Post a Comment