Writing a Profiler for Silverlight 4

The Silverlight 4 beta has been released a while ago (see this), and one of the new features in Silverlight 4 is the ability to use the very same profiling API that is available for regular CLR-based apps (referred to as “desktop” CLR apps).  In this post I’ll talk about how to create a profiler that uses the CLR profiling API on Silverlight 4.  I assume you are already familiar with creating profilers for the desktop CLR.

Getting Started

Requirements

Install Silverlight 4 beta!  (See link above.)  It is sufficient to simply install the Windows Silverlight Developer runtime.  You don’t need the full Silverlight 4 Beta Tools for Visual Studio 2010 to write and test your profiler.

Note that, in order to have full functionality, you and your users will need the Windows Silverlight Developer runtime, and not just the typical Windows Silverlight runtime.  The reason is that some of the profiling API methods require the debugging infrastructure to be completely initialized and available, and that is ensured by installing the Windows Silverlight Developer runtime.  For example, if you attempt to call SetILInstrumentedCodeMap() or GetILToNativeMapping() without a fully initialized debugging infrastructure, then they will fail with CORPROF_E_DEBUGGING_DISABLED.  Your main concern, though, should be that our testing of the profiling API on Silverlight is done with a Developer runtime only.  So we can’t comment on how well or poorly profiling will go without the Developer runtime.  Also, the number of profiling API methods that will fail on Silverlight without the Developer runtime may change at any time without notice (or without us even realizing).

It is expected that, in most scenarios, it should not be too much of a burden to require the Silverlight Developer runtime, as your users will typically be developers of Silverlight applications (who already need the Developer runtime anyway).  However, it is true that some profiling API-based tools are not really targeted at developers (e.g., some tools may monitor the flow of multi-tiered applications throughout an enterprise, including the end user client Silverlight tier).  So for those of you in that situation, you will need to take care to ensure your users have installed the Developer runtime.  A member of the Silverlight team tells me a good way to do this is to check for the existence of this registry key/value:

HKEY_LOCAL_MACHINE\software\(Wow6432Node on 64-bit boxes)\microsoft\silverlight\Components\Debugging
    Version    REG_SZ    4.0.50113.0

You should also ensure that the above Version string matches the Version value of the runtime which is stored in the grandparent key:

HKEY_LOCAL_MACHINE\software\(Wow6432Node on 64-bit boxes)\microsoft\silverlight
    Version    REG_SZ    4.0.50113.0

If a user had the Developer runtime installed and “upgraded” to a newer end-user runtime the registry will still report the Debugging components but the version will be the old version, which for our purposes is the same as not having a Developer runtime installed at all.  Doing the comparison above ensures you catch that case, and can tell your user to install the latest Developer runtime.

Coding and Activation

A philosophy we have with enabling the profiling API on Silverlight is that it should be easy to reuse code and binaries from desktop CLR profilers, but we still wanted to ensure that a desktop profiler does not accidentally get activated in a Silverlight app.  To that end, we’ve kept the interfaces and their IIDs the same, but we’ve changed the environment variables.

So, you will not need to create another copy of your callback interface implementation, or keep separate IIDs around for the Info interfaces you query for.  Your very same CLR V4-desktop-based code will now target Silverlight 4 apps as well.

In order for your profiler to be activated against Silverlight, you will need to use the following environment variables:

CORECLR_ENABLE_PROFILING same meaning as COR_ENABLE_PROFILING has on desktop
CORECLR_PROFILER same meaning as COR_PROFILER has on desktop
CORECLR_PROFILER_PATH same meaning as COR_PROFILER_PATH has on desktop (this is optional, if you want to use registry-free activation)

By keeping the environment variables different between Silverlight and desktop, we not only prevent desktop profilers from being accidentally activated in Silverlight apps, but we also prevent a profiler from accidentally getting instantiated twice in some processes.  Imagine: Someone writes a desktop CLR app that renders web pages, and inside one of those web pages is a Silverlight control that hosts Silverlight apps.  Or perhaps Internet Explorer is rendering a Silverlight page in one tab and a page with a CLR Click-Once app in another tab.  If a single set of environment variables controlled both desktop and Silverlight apps, then a profiler would get instantiated twice.  This is not necessarily bad—unless the profiler was not prepared for such an activation.  If you carefully code your profiler to avoid most global state (as would be necessary for enabling profiling of multiple in-process side-by-side CLR instances), then your profiler might well be fully capable of supporting two instances in the same process—one working with the Silverlight runtime, and the other working with a desktop CLR.  In such a case, feel free to set all environment variables (both the CORECLR_* and COR_* flavors) to enable simultaneous profiling of desktop and Silverlight runtimes, if that’s something you’d like to support.

Behavioral Differences Between Silverlight and Desktop Profiling

Although your very same code may be used to target both Silverlight and Desktop, you will likely need to have some conditional logic to deal with some behavioral differences between the two platforms.  (You may use ICorProfilerInfo3::GetRuntimeInformation to determine whether a given runtime is the desktop CLR (COR_PRF_DESKTOP_CLR) or Silverlight CLR (COR_PRF_CORE_CLR).)

No attach / detach on Silverlight.  Although the ability to attach to and detach from running processes is a new feature enabled on CLR V4, this feature is not available on Silverlight 4.

Don’t rely on receiving the Shutdown() callback on Silverlight.  Really, you can’t rely on Shutdown() on the desktop CLR either, as per the MSDN topic, though this callback looks to be even more unreliable on Silverlight.  Your profiler receives the Shutdown() callback depending on how the CLR is terminated, and it’s looking like Silverlight terminates the CLR in the “abrupt” fashion where most shutdown logic is skipped (including the call to the Shutdown() profiler callback).  So it will be best if you use your DllMain as a backup for ensuring your cleanup code gets run.

No event logging.   Silverlight generally does not use the Windows event log for anything.  When developing (or using) a profiler on the desktop CLR, the event log is useful for detecting and diagnosing problems with loading the profiler.  On Silverlight, these messages are routed to your debugger if you’re debugging the process hosting Silverlight, and the messages are not sent anywhere if you’re not debugging.  That means that, if you’re having issues diagnosing activation problems with your profiler, run the Silverlight process under a debugger like VS or windbg and look in the output window for messages that indicate whether Silverlight attempted to load a profiler, whether the load succeeded, and if not, why.

IL Rewriting

Doing run-time IL rewriting (or “instrumentation”) on Silverlight has enough differences from desktop that it warrants its own section.  As a review, the APIs involved in IL Rewriting are described here.  You will generally use these same APIs on Silverlight, but will encounter differences when your rewritten IL tries to do stuff, like call into your own managed helper assembly.

Security

First, realize that user Silverlight assemblies generally run under partial trust, with fairly restricted permissions.  (Full trust is reserved for the Microsoft-provided platform code, such as mscorlib.dll.)  So when you instrument partial trust code, keep in mind your rewritten IL will be under those same restrictions.  It is therefore best to avoid doing any security-sensitive operations, or if necessary, moving those operations to new Critical methods you dynamically add to mscorlib, with SafeCritical bridge code in the middle.  Read the security section from this blog post for more information.

Shipping Managed Helper Code

Many profilers rewrite user IL to call into managed “helper code” shipped by the profiler vendor.  This helper code is usually a centralized place to perform whatever logging is necessary to record that certain events have occurred (e.g., a call was made, a local variable was modified, etc.).  On Silverlight, you have a couple options on how to ship this helper code.

Option 1: Pump helper code into mscorlib

As you may recall, when profiling desktop CLR apps, it is illegal to add a reference from mscorlib to any other assembly.  Therefore, if your profiler instruments mscorlib methods, then that rewritten IL is forbidden to directly call into a separate helper assembly.  One workaround for this is for all helper code to be added into mscorlib at runtime via IMetaDataEmit.  This workaround is valid on Silverlight as well, and is therefore a perfectly valid option for how to ship your helper code—just pump it into mscorlib at run-time.  You will need to do this sort of thing anyway if any of your helper code needs to run at full-trust (e.g., if it needs to P/Invoke).

Option 2: Ship separate helper managed module that you inject into the XAP.

If your profiler does not need to instrument mscorlib, and can get its work done using partial-trust code (which is preferable), then option 1 is still a reasonable solution.  But your profiler may also do the following, which you may decide is easier to manage:

  1. Develop, compile, and ship a helper managed module containing helper methods that the instrumented code calls into (just like on desktop)
  2. Get this helper managed module into the XAP somehow (see below)
  3. Instrument user code to call into your helper managed module, the usual way (using IMetaDataEmit to generate an AssemblyRef and any necessary TypeRefs, MemberRefs, etc., from the user’s module to your helper managed module).

For achieving step 2, you have a couple options:

(2a) (preferred): If at all possible, you may wish to integrate your profiler with Visual Studio so that the build process itself can ensure the profiler's helper managed module finds its way into the XAP.  (More on this below.)

(2b) (fallback): There will likely be scenarios where profilers will not be able to participate in the build process.  In these cases, the profiler's helper managed module must find its way into the XAP after the XAP has been built.  In order to do this, you simply treat the XAP file like the zip file it really is:

(i) Unzip the XAP
(ii) Add your managed helper module to the XAP
(iii) Modify the XAP's manifest (this is a special file in every XAP that lists the assemblies contained in the XAP)
(iv) Rezip the XAP

For (2a) (getting the VS build system to include your helper managed module in the XAP), here is what I learned from the Silverlight tools folks…

The items that get added to the XAP at build-time are roughly the following:

  1. Built assembly
  2. AppManifest.xaml
  3. References marked CopyLocal and their dependencies. If you have enabled “Reduce XAP size by using application cacheing”, assemblies that support this feature will not be included in the XAP (they get their own zip file).
  4. Any project items marked Content
  5. Satellite assemblies built by the project, or picked up by references, that match the <SupportedCultures> property.

The easiest way to get your helper managed module into the XAP file is probably to add it as a Content file to the project.  For example:

  1. Create a new Silverlight project
  2. Edit the project file to uncomment the BeforeBuild target and add the following to it:

<Target Name="BeforeBuild">
    <ItemGroup>
        <ContentWithTargetPath Include="PathToMyHelperManagedModule.Dll" />
    </ItemGroup>
</Target>

The key here is that we’re adding the DLL to the ContentWithTargetPath item collection, the contents of which get added to the XAP file.  You should also be able to leverage msbuild if you like.  

IE8 & child processes

Starting with Internet Explorer 8, tabs may be rendered via child processes, and not the iexplore.exe that was originally spawned by the user.  Some profiler products use a GUI shell to spawn the profilee (in this case the profilee would be iexplore.exe) and then communicate with the profiler DLL inside that process to allow the user to view and control information about the running application.  With IE8, the iexplore.exe that’s spawned by your shell may not be the process rendering the Silverlight control (and thus loading your profiler DLL).  So if you use such an architecture, and if your GUI shell needs to know the process ID of the process actually rendering the Silverlight application (and thus loading your profiler), this wouldn’t work out so well.  Thus, instead of directly spawning iexplore.exe, your shell should instead use IELaunchURL() to spawn IE and tell you the real ID of the process actually rendering the Silverlight app.

 

There you go!  If you’ve ever thought about modifying your profiler to target Silverlight, now is the time to download the Silverlight 4 Beta and give it a whirl.  Hopefully you should be able to keep most of your code intact, only customizing specific code paths that need to be different on Silverlight.