Click here to Skip to main content
15,883,883 members
Articles / COM

ChartPoints

Rate me:
Please Sign up or sign in to vote.
3.67/5 (2 votes)
16 Dec 2017CPOL14 min read 7.1K  
MSVS2015 chart view trace extension

Background (Motivation Part)

This is a technical description of ChartPoints MSVC 2015 extension.

All user information is can be found here.

Source code: https://github.com/alekseymt74/ChartPoints

Postulate #1: “Tracing is helpful”.

All of us are using it from time to time. Me too.

To achieve this goal, predefined IDE macros/classes, third-party libraries, own techniques are commonly used. One of the main problems is that we need to do it manually in code.

This leads to Postulate #2: “All that can be automated must be automated.”

Postulate #3: “Sometimes quick look from afar helps to find problematic areas easier than digging in details”.

Conclusions

  1. Give user the possibility to add trace points from IDE (inspired by Postulate #2)
  2. Provide interactive charts with traced data (inspired by Postulate #3)
  3. Minimize overhead

Important Notes (Justification Part #1)

Note #1

It’s only the first experimental stage (cycle) with many limitations. Some parts need to be refactored/changed, e.g., choice of COM EXE server as transport, but I didn’t refuse to its usage in order to understand this more clearly. J

Note #2

My knowledge of C# is far from professional level. There were two reasons to use it:

  • It’s easier to develop MSVS extension using it than to develop in native C++
  • It was interesting to me

Hence the conclusion: please be lenient.

Note #3

This project contains many stand-alone parts, which will not be fully covered in this article. I will make common architecture overview and then concentrate on most significant aspects. If this is not enough, please mention this in comments. If there is interest, I will gather all questions and write “Part 2”.

Note #4

I’ll not describe how to create tool windows, context menu handler, e.g. This is fully described in many samples. First of all, in VSSDK-Extensibility-Samples, I’ll concentrate on most difficult (IMHO) parts.

Note #5

My English is not as good as I’d like it to be. Sorry.

Limitations (Justification Part #2)

  • Supported languages: native C++ only
  • Supported IDE: MSVC 2015
  • Tracing variables: class members of C++ fundamental types only + some typedefs like std::uint32_t (because they are useful)

Used Technologies/Languages

  • C++ (injected code, COM out-of-proc server)
  • C# (MSVS 2015 extension, MSBuild task)

Used Architecture Patterns

Two main common architecture patterns are used in this project: SOA (Service-Oriented Architecture) & EDA (Event-Driven Architecture). Both patterns are long known and well described, so I’ll focus only on the details of current implementation.

SOA (Service-Oriented Architecture)

Declaration (ICPServiceProvider.cs)
C#
  // base interface for all ChartPoints services (maybe extended in the future)

  public interface ICPService
  {
  }

  //public delegate void OnCPServiceCreated<T>(T args);
  // singleton service provider
  public abstract partial class ICPServiceProvider
  {
    public abstract bool RegisterService<T>(T obj) where T : ICPService;
    public abstract bool GetService<T>(out T obj) where T : class;
    //public abstract bool GetService<T>(out T obj, OnCPServiceCreated<T> cb) 
    //                                   where T : class;
    public static ICPServiceProvider GetProvider()
    {
      return impl.ICPServiceProvider.GetProviderImpl();
    }
}
Implementation (CPServiceProvider.cs)

Service provider stores registered services on calling RegisterService for future return by GetService. Second (commented) version of GetService will allow to provide callback which will be used if service is not registered yet. There is no reason to implement it now so I left it commented.

This approach allows to manage the order of service creation & query services from any place without worrying about their construction.

EDA (Event-Driven Architecture)

Declaration (ICPEventService.cs)
C#
public delegate void OnCPEvent<T>( T args );

public abstract class ICPEvent<T>
{
  protected abstract ICPEvent<T> Add(OnCPEvent<T> cb);
  public static ICPEvent<T> operator +(ICPEvent<T> me, OnCPEvent<T> cb)
  {
    return me.Add(cb);
  }

  protected abstract ICPEvent<T> Sub(OnCPEvent<T> cb);

  public static ICPEvent<T> operator -(ICPEvent<T> me, OnCPEvent<T> cb)
  {
    return me.Sub(cb);
  }

  public abstract void Fire(T args);
}
Implementation (CPEventService.cs)

ICPEvent provides usual +/- operators. In the current implementation, all events are stored (there are not so many of them so we don’t have to worry). When new client subscribes, it receives all earlier generated events. If it will be required, simple specialization without history will be added. Current implementation provides all the needs.

It’s done for two reasons:

  1. Some objects are initialized not in predefined order (e.g., from event handlers of MSVS) and this guarantees that all events will be delivered to recipients.
    Example: When tagger (object responsible for rendering glyphs in code editor) is created and subscribes to ChartPoints events, it successfully receives all fired earlier events and has all actual information for rendering glyphs.
  2. It gives a possibility to add time marks to make them as part logging system (not implemented now, but can be easily added).

Class Factory

Basic goals of current class factory implementation:

  1. Allows to implement class factories based on any of existing ones (partial extension) and provide moqs & stubs for testing.
  2. Strategy pattern via DI. Allows implementing different strategies of same interface. As all objects query instances via factory methods, all that is need to do is to change appropriate class factory method for new object implementation.
  3. Hide the possibility of explicitly creating objects intended to be created via class factory (in same assembly too). It was hard to understand how to achieve this. In C++, it is done easily, but in C#, redundant entities need to be added to emulate friend keyword. Maybe, it can be done more easily.
Declaration of class factory (IClassFactory.cs)
C#
  // class factory interface
  public abstract partial class IClassFactory
  {
    // set custom class factory instance for DI purposes
    public static void SetInstance(IClassFactory inst)
    {
      ClassFactory.SetInstanceImpl(inst);
    }

    // returns singleton instance
    public static IClassFactory GetInstance()
    {
      return ClassFactory.GetInstanceImpl();
    }

    // factory methods
    public abstract IChartPointsProcessor CreateCPProc();

    <..>

  }

Classes Intended to be Constructed via Class Factory Declaration

Interface declaration
C#
public interface IChartPointsProcessor
{
  <..>
}
Implementation

Important: Marked abstract to hide possibility to explicitly construct it. Only derived classes can do that.

C#
  public abstract class ChartPointsProcessor : IChartPointsProcessor
  {
    <..>
}
Implementation of class factory (ClassFactory.cs)
C#
public abstract partial class IClassFactory
{
  // implementation of ordinal class factory
  // is accessible only by IClassFactory and class factory implementations
  // (for DI purposes)
  private partial class ClassFactory : IClassFactory
  {
    public ClassFactory()
    {
      <..>
    }

    private static IClassFactory Instance;

    public static void SetInstanceImpl(IClassFactory inst)
    {
      Instance = inst;
    }

    public static IClassFactory GetInstanceImpl()
    {
      if (Instance == null)
        Instance = new ClassFactory();
      return Instance;
    }

    // IChartPointsProcessor factory
    // Opens the back-door to construct ChartPointsProcessor object
    // To  access non-default constructors
    // appropriate delegating ones need to be added
    private class ChartPointsProcImpl : ChartPointsProcessor { }

    // IChartPointsProcessor factory method implementation
    public override IChartPointsProcessor CreateCPProc()
    {
      return new ChartPointsProcImpl();
    }

    <..>
}
Extended Class Factory (Example)
C#
  // IChartPointsProcessor implementation for dependency injection
  namespace impl
  {
    namespace DI
    {
      // Can be fully implemented from IChartPointsProcessor 
      // or extend any existing implementation
      public class DIChartPointsProcessor : ChartPointsProcessor
      {
        // methods to override
      }
    } // namespace DI
  } // namespace impl

  // dependency injection class factory implementation
  public abstract partial class IClassFactory
  {
    // Can be fully implemented from IClassFactory or extend existing one
    // In this case ordinal class factory used 
    // to override IChartPointsProcessor factory method only
    class DIClassFactory_01 : ClassFactory
    {
      private class ChartPointsProcImpl : DIChartPointsProcessor { }
   
      // IChartPointsProcessor factory method implementation
      public override IChartPointsProcessor CreateCPProc()
      {
        return new ChartPointsProcImpl();
      }
  }

  // instantiate di class factory. Needed because 
  // IClassFactory.DIClassFactory_01 declaration is inaccessible explicitly
  public static IClassFactory GetInstanceDI_01()
  {
    return new DIClassFactory_01();
  }
}

Somewhere (before construction of other class factory objects started), call:

C#
Utils.IClassFactory diCF = Utils.IClassFactory.GetInstanceDI_01();

// after calling this all objects will be constructed via this class factory
Utils.IClassFactory.SetInstance(diCF);

How It Works

Quick View

ChartPoints
  • User friendly interface for adding ChartPoints (aka breakpoints)
  • Taggers in code editor indicating their placement
  • List of ChartPoints in special tool window
  • Simple code text changes listener (aka breakpoints)
  • Save/Load defined ChartPoints
  • Separate ChartPoints mode from ordinal builds
  • ChartPoints validation before build, before/after save/load
  • User interactive chart view
  • Table view of ChartPoints values based on their generation time
Code Generation
Trace Library (Publisher Side – Traced Program)

Minimize overhead between traced & untraced code execution. It is very important because if they differ much, it will provide different behavior and tracing will become meaningless.

Trace Library (Consumer Side - Host)

The requirements on this side are not as strict as on publisher’s. The main goal of this project is to perform post analysis. So some lag in run-time allowed.

Trace Transport

As mentioned earlier, I decided to use COM EXE server as transport layer between the program being traced and the host. As it seems to me, this wasn’t a good idea and needs to be changed in the future. I’m planning to change transport layer later. So I will not describe it in detail.

Description

Step #1 (Selection of ChartPoints)

Visual Studio contains a set of interfaces for manipulating language code model.

As I indicated in Limitations section, only class variables of C++ fundamental types are allowed to be traced. So the only one place where it can be done is class method definition.

Before showing context menu in code editor, checking availability of adding ChartPoint in performed. This is done in CP.Code.Model.CheckCursorPos() method. From EnvDTE.ActiveDocument current cursor position (EnvDTE.ActiveDocument.Selection.ActivePoint) and FileCodeModel (EnvDTE.ActiveDocument.ProjectItem.FileCodeModel) are acquired. Using FileCodelModel.CodeElementFromPoint method, cursor position check is performed: being inside method body. If so, Parent property of returned CodeElement points to VCCodeClass object, which is used to get all class variables. The future injection point will be at the beginning of the line or immediately after open brace of method if cursor is on line containing it.

One ChartPoint can contain multiple traceable variables.

All set ChartPoints are added to “ChartPoints:design” tool window.

Brief ChartPoints Classes Architecture

Image 1

All ChartPoints data is stored in tree structure. These objects provide events for subscribing on their Add/Move/Remove/Status changes. This objects composition gives the possibility to easily operate them in forward (from root to leafs) and backward (based on events notifications) order. Both approaches will be actively used further.

Step #2 (Taggers)

VSSDK-Extensibility-Samples contains samples showing basic usage of taggers. Also MSDN has several articles describing it. The beginning point is here: Inside the Editor.

But I want more:

  1. Force taggers appearance/change
  2. Optimize performance (exclude redundant updates)
Short Taggers Overview

Every time new document is opened/changed, MSVS calls custom IViewTaggerProvider implementation (if any) method CreateTagger to create ITagger object. It’s method GetTags will be later called from MSVS environment in order to determine if (and where) tags are present.

Problem

IViewTaggerProvider.CreateTagger is called multiple times for the same document. It looks like it called for each window that can contain tags: code editor, find results (???). As I found the last one is called for code editor window. Yes, it’s works but I don’t have full understanding. So this needs to be researched more clearly.

Custom Taggers Implementation

Image 2

All created tagger are stored in association array with file names as keys. ChartPoints tagger provider subscribes on IFileChartPoints Add/Remove events with subsequent providing IFileChartPoints object to stored taggers. This gives them possibility to subscribe on ILineChartPoints events notifications.

When document is opened for the first time or changed, GetTags method of ChartPoints tagger is called. In this method, intersection of lines in SnapshotSpan and stored containing ChartPoints numbers calculated.

If needed to update tag manually from outside, IChartPointsTagger.RaiseTagChangedEvent is fired with parameter containing line number. ITagger<ChartPointTag>.TagsChanged event with SnapshotSpan containing only 1 line, where ChartPoint is placed, fired. This helps to exclude redundant checks on tags creation and provides the possibility to force (re-)draw tags.

Important: All indexes (line/character numbers) used here are 0-based. EnvDTE indexes, which are used to calculate ChartPoints positions, starts from 1. And this is the cause of constant headache.

Step #3 (Save/Load ChartPoints)

All information about contained ChartPoints is saved per solution basis in *.suo (Solution User Options) file.

In order to do it, I use implementation of IVsPersistSolutionOpts interface which provides overloaded methods and a reference to Microsoft.VisualStudio.OLE.Interop.IStream object. On load, this object is cloned & stored for use after solution loaded.

Step #4 (Text Changed Tracker)

Code changes are tracked only by simple text changes now. Perhaps this is enough. I experimented slightly with VCCodeModel but decided that it is too complicated & expensive.

Tracking system divided into two parts: UI (MSVS side) and Model (ChartPoints). It was done so when I thought that I will use both text change listeners & code model changes. Maybe someday, I will return back to this.

UI

MSVS services for listen text changes are: IWpfTextViewCreationListener and IWpfTextView. Implementation of the first one provides handle TextViewCreated(IWpfTextView) event. The second gives the possibility to subscribe to ITextBuffer.Changed event.

Model

ICPTrackService tracks ChartPoints Add/Remove events and provides small wrapper objects which hides ChartPoints objects references. This service and several events bind UI & Model.

ChartPoints track service sequence diagram:

Image 3

IWpfTextViewCreationListener.TextViewCreated(IWpfTextView) is called for each opened document. If no FileTracker objects for this file registered in ICPTrackService, TextChangeListener will store IWpfTextView object within filename. Later, if Model.FileTracker create event will be received, FileChangeTracker object with references to IWpfTextView & FileTracker will be created. It will subscribe to buffer changed event and query validation from FileTracker.

Step #5 (Code Instrumentation)

Code instrumentation is performed via MSBuild task placed in CPInstBuildTask.dll.

MSVS Host

When start building (Globals.dte.Events.BuildEvents.OnBuildProjConfigBegin event handler), following actions are performed:

  1. Check the existence of ChartPoints in current project.
  2. Disable debug information generation (it is not needed because after instrumentation, executing code will differ from the original one):

Here is the code doing it:

C++
EnvDTE.Project proj = ..
<...>
VCProject vcProj = (VCProject)proj.Object;
VCConfiguration vcConfig = vcProj.Configurations.Item(projConfig);
IVCCollection tools = vcConfig.Tools as IVCCollection;
VCLinkerTool tool = tools.Item("VCLinkerTool") as VCLinkerTool;
tool.GenerateDebugInformation = false;
  1. Validate ChartPoints.
  2. Transport for communication between MSVS host and MSBuild task is opened. ServiceHost with NetNamedPipeBinding is used for this. Why? It was the first that I saw starting to dig C# capabilities, :). As an address project file, full name is used. It looks ugly but gives unique address. Maybe someone someday will decide to synchronously build the same project from several instances of MSVS and problems will be guarantied. But I believe in the power of reason.

MSVS host provides IPCChartPoint interface (and few others placed in the same file) which contains method for calculating ChartPoints injection layout for injection points:

  1. Trace variables initialization
  2. Trace points
  3. Additional include file injection
  4. .. and so on
MSBuild Task
  1. Open ServiceHost with same address.
  2. Acquire IPCChartPoint object.
  3. Calculate injection points layout IPCChartPoint.GetInjectionData(<project name>).
  4. Copy required source files to %TEMP% directory
  5. Instrument them (see detailed information in the next section Step #6).
  6. Pass instrumented files to MSBuild (add them to build and remove ordinal ones from build).

Step #6 (C++ Tracing Library)

To organize the correct variables tracking, the following data is required:

  1. Identifier of traced variable.
    The variable address casted to unsigned 64-bit value used for this purpose (*)
  2. Variable name
  3. Type id
    For further usage. Not used now.
  4. Variable value
  5. Timestamp
    It’s taken at moment of tracing to provide reliable information

(*) It is guaranteed that in current moment, address value is unique. But the key phrase is “in current moment”. Same address can be used for multiple times. It depends on variable lifetime. Workaround of this issue will discussed later in “Partially unique identifier” section.

Predefined entities used for code instrumentation:

  1. cpti(64).dll libraries containing tracing logic. Are explicitly loaded by injected code.
  2. __cp__.tracer.h
    Declares type id wrapper class type_id and class tracer which methods are used by instrumented code. It is implemented in cpti(64).dll libraries.
  3. __cp__.tracer.cpp
    Template specializations of type_id class for all supported types used in tracing
    Implements tracer_ptr tracer::instance() method which loads cpti(64).dll depending on Platform (x86/64) used by instrumented module.
Instrumentation Details

#include "__cp__.tracer.h" is added to the beginning of each instrumented file. As instrumented files are the copies of ordinal ones, all their include occurrences are changed to use new ones.

__cp__.tracer.cpp is added to project.

Registration
C++
tracer::pub_reg_elem("test_01::d_01",d_01);

Type and variable identifiers are generated on the fly. First – using specialized version of type_id class. Second by taking and casting variable address.

Tracing
C++
tracer::pub_trace(d_01);
Under the Hood (tracer_impl.cpp)

As I mentioned earlier, one of the main requirements is to minimize overhead.

The following class is used for this purpose:

C++
template<typename TData>
class data_queue
{
public:
  typedef std::queue< TData > data_cont;
private:
  data_cont data_1;
  data_cont data_2;
public:
  data_cont *in_ptr;
  data_cont *out_ptr;

  data_queue()
  {
    in_ptr = &data_1;
    out_ptr = &data_2;
  }

  void swap()
  {
    std::swap( in_ptr, out_ptr );
  }
};

When tracer::pub_trace is called, access to data_queue object is locked and new value with id and timestamp is added to the queue::in_ptr queue.

Passing of stored values to transport is performed in a separate thread, which locks access to data_queue object only for data_queue::swap() which swaps queue::in_ptr & queue::out_ptr pointers. Then, this thread selects all the data from the queue pointed to by queue::out_ptr and passes it to transport.

This gives the possibility to block calling (trace source) thread for minimal time.

Partially Unique Identifier

As I said, “Same address can be used for multiple times” and it complicates its usage as identifier. But statement “in current moment” helps.

It is resolved in the following way:

One more thread(tracer_impl::reg_proc) and data_queue are created. It works in common the same way as data tracing except one important moment: before any registration is performed, all accumulated data must be sent.

For this purpose, utility class notifier is used. It works like boost::barrier/semaphore (both are based on waiting on desired counter value). Each time tracer_impl::reg_elem is called, registration info (sic, containing registration timestamp) is stored, notifier object counter is increased and waiting registration thread is notified. Registration thread publishes all accumulated tracing data preceding current registration entity timestamp. After this is done, it decreases notifier object counter. All this time data sending thread is sleeping waiting for notifier zero counter value. It provides the current order of reg/trace messages delivery.
Note: The host (MSVS ChartPoints extension) knows about it, which helps it to correctly handle received messages.

Step #7 (COM EXE Server, CPTracer.exe)

Choice of COM out-of-proc server as a transport was a temporary experiment in order to estimate its capabilities. But as is well known, "there is nothing more permanent than a temporary"©. So it accompanied me all the time developing the first version, :). And I will refuse using it at the first opportunity. That's why I’ll say about it usage only few words.

The same data_queue class instance used for sending data to customer. It’s done to decrease COM events delivery calls. Sending thread sleeps for 500ms on each iteration and then compose all data to arrays that are sent to customer in one call.

Future Plans (Not In Order of Priority)

  • Refuse COM server usage. Move to some net protocol.
  • Move logic described in Step #6 (C++ tracing library) to customer.
  • Add tracing capabilities of local variables.
  • Remove “<..> [ChartPoints]” configuration. Use MSBuild manually without changing origin *.sln & *.vcxproj files.
  • Move ChartPoints storage to separate configuration files.
  • Relax... Looks like a candidate for number one in the list of priorities.

History

  • 17th December, 2017: Initial version

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer
Russian Federation Russian Federation
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
-- There are no messages in this forum --