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
- Give user the possibility to add trace points from IDE (inspired by Postulate #2)
- Provide interactive charts with traced data (inspired by Postulate #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
typedef
s 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)
public interface ICPService
{
}
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 static ICPServiceProvider GetProvider()
{
return impl.ICPServiceProvider.GetProviderImpl();
}
}
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)
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);
}
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:
- 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. - 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:
- Allows to implement class factories based on any of existing ones (partial extension) and provide moqs & stubs for testing.
- 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.
- 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.
public abstract partial class IClassFactory
{
public static void SetInstance(IClassFactory inst)
{
ClassFactory.SetInstanceImpl(inst);
}
public static IClassFactory GetInstance()
{
return ClassFactory.GetInstanceImpl();
}
public abstract IChartPointsProcessor CreateCPProc();
<..>
}
Classes Intended to be Constructed via Class Factory Declaration
Interface declaration
public interface IChartPointsProcessor
{
<..>
}
Implementation
Important: Marked abstract to hide possibility to explicitly construct it. Only derived classes can do that.
public abstract class ChartPointsProcessor : IChartPointsProcessor
{
<..>
}
Implementation of class factory (ClassFactory.cs)
public abstract partial class IClassFactory
{
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;
}
private class ChartPointsProcImpl : ChartPointsProcessor { }
public override IChartPointsProcessor CreateCPProc()
{
return new ChartPointsProcImpl();
}
<..>
}
Extended Class Factory (Example)
namespace impl
{
namespace DI
{
public class DIChartPointsProcessor : ChartPointsProcessor
{
}
}
}
public abstract partial class IClassFactory
{
class DIClassFactory_01 : ClassFactory
{
private class ChartPointsProcImpl : DIChartPointsProcessor { }
public override IChartPointsProcessor CreateCPProc()
{
return new ChartPointsProcImpl();
}
}
public static IClassFactory GetInstanceDI_01()
{
return new DIClassFactory_01();
}
}
Somewhere (before construction of other class factory objects started), call:
Utils.IClassFactory diCF = Utils.IClassFactory.GetInstanceDI_01();
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
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:
- Force taggers appearance/change
- 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
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:
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:
- Check the existence of
ChartPoints
in current project. - 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:
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;
- Validate
ChartPoints
. - 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:
- Trace variables initialization
- Trace points
- Additional include file injection
- .. and so on
- Open ServiceHost with same address.
- Acquire IPCChartPoint object.
- Calculate injection points layout IPCChartPoint.GetInjectionData(<project name>).
- Copy required source files to %TEMP% directory
- Instrument them (see detailed information in the next section Step #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:
- Identifier of traced variable.
The variable address casted to unsigned 64-bit value used for this purpose (*) - Variable name
- Type id
For further usage. Not used now. - Variable value
- 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:
- cpti(64).dll libraries containing tracing logic. Are explicitly loaded by injected code.
- __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. - __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
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
tracer::pub_trace(d_01);
As I mentioned earlier, one of the main requirements is to minimize overhead.
The following class is used for this purpose:
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