Designing Stacktraces for Xamarin Insights

January 2016

Xamarin Insights is an app monitoring service that reports crashes and analyzes user behavior for apps built with Xamarin Platform. When we designed Insights, we wanted to render the most useful and beautiful stacktraces of any similar service. I’d like to explain the surprisingly sophisticated technology we deployed to render deceptively simple stacktraces.

High-level Goals

Our main goal when rendering stacktraces is to display precisely the information that will help the developer most quickly identify the relevant source code, and attempt to reconstruct a clear picture of the state of execution when the crash occurred. Towards this main goal, we listed several high-level corollaries:

  1. Details not relevant to debugging the crash should be hidden or minimized, so that relevant details are more apparent.
  2. Stacktraces should resemble the host language (usually C#), translating arcane low-level representations into something more familiar to the programmer debugging the crash.
  3. The stacktrace should be stylized with color and typography to increase legibility.

Raw Material

Here is an example of a raw stracktrace reported by the Insights client from a Xamarin app written in C#:

Initial observations:

It became clear that we would lose our minds long before our fragile regular-expression-based rendering pipeline could be extended to address these issues.

Technical Requirements

My designs for how the stacktraces should appear proved divisive because I wanted to aggressively minimize what I considered to be extraneous details in the raw stacktrace, and the engineering team couldn’t reach consensus. A sad truth about being a designer is that no matter how well you argue that your design is correct, if an engineer disagrees and produces a working implementation for their preferred design, your argument loses much of its force. I used this sad truth to my advantage this time, offering to implement the stacktrace renderer rather than merely arguing for my technically difficult design. Perhaps this is a small failure as a designer, but nevertheless a victory for the user.

Thankfully, I had a lot of latitude for the implementation. I gathered some high-level ‘requirements’:

Choosing PureScript

These requirements lead me to choose PureScript, “a small strongly typed programming language that compiles to JavaScript,” with monad syntax and a fledgling parser combinator library.

Here’s some sample PureScript code from the library. fileLocationParser is a purely functional, independently testable parser that parses a FileLocation from a String, first by trying to parse a UNIX-style location, and, if that fails, backtracking and attempting to parse a Windows-style location:

fileLocationParser = try unixFileLocationParser <|> windowsFileLocationParser

unixFileLocationParser = do
  file <- untilChar ":"
  string ":"
  optional $ string "line "
  lineNumber <- digits
  return $ FileLocation file (readDecimal lineNumber)

windowsFileLocationParser = do
  drive <- alpha
  string ":"
  file <- untilChar ":"
  string ":line "
  lineNumber <- digits
  return $ FileLocation (drive <> ":" <> file) (readDecimal lineNumber)

I hope you can see that monadic parsing with combinators is more modular and maintainable than regular expressions, and high-level enough so that most programmers can understand what’s happening.

Transformations

Since the parser builds an abstract syntax tree of the stacktrace, it’s straightforward to apply transformations to this tree before it’s rendered. Some of the transformations we apply:

For example, here’s a transformation that uses pattern matching to rewrite async invocations:

-- If an invocation is a lifted async, rewrite its method and drop the compiler genereted child class
unliftAwait :: Invocation -> Maybe { invoke :: Invocation, awaitN :: Int }
unliftAwait (Invocation typ meth) = do
  info <- unlift typ
  let meth' = method info.methodName Nil
  let invoke' = Invocation info.newType meth'
  return { invoke: invoke', awaitN: info.awaitN }

  where
  unlift :: Type -> Maybe { newType :: Type, methodName :: String, awaitN :: Int }
  unlift
    (Type t@{
      child: Just
        (Type {
          child: Nothing,
          name: LiftedAsync methodName awaitN
        })
    }) = Just { newType: Type (t { child = Nothing }), methodName: methodName, awaitN: awaitN }

  unlift (Type t@{ child: Just child }) = do
    info <- unlift child
    let t' = t { child = Just info.newType }
    return $ info { newType = Type t' }

  unlift _ = Nothing

The Result

The entire library, including parsing, transformation passes, and HTML/JSON output is 955 lines with an average line length of 34 characters. It’s a delight to maintain and deploys as a single JavaScript file.

Here’s the result of parsing and rendering the sample stacktrace mentioned earlier:

Note that,

And here’s a screenshot of how stacktraces appear in the product:

Like I said, a hell of a lot of technology behind a deceptively simple design!

Final Considerations and Future Work

The monadic code performs adequately but is still much slower than a hand-built JavaScript parser should be, often taking over 50ms to parse a large stacktrace. I expect future generations of the PureScript compiler to improve this.

In the future, I’d like to design responsive stacktraces, that hide or show more or less information depending on viewport width.