Andrei Chiş
email: andrei@inf.unibe.ch

Marcus Denker

Tudor Gîrba

Oscar Nierstrasz

Practical domain-specific debuggers using the Moldable Debugger framework

Keywords: debugging, customization, domain-specific tools, user interfaces, programming environments, Smalltalk

Understanding the run-time behaviour of software systems can be a challenging activity. Debuggers are an essential category of tools used for this purpose as they give developers direct access to the running systems. Nevertheless, traditional debuggers rely on generic mechanisms to introspect and interact with the running systems, while developers reason about and formulate domain-specific questions using concepts and abstractions from their application domains. This mismatch creates an abstraction gap between the debugging needs and the debugging support leading to an inefficient and error-prone debugging effort, as developers need to recover concrete domain concepts using generic mechanisms. To reduce this gap, and increase the efficiency of the debugging process, we propose a framework for developing domain-specific debuggers, called the Moldable Debugger, that enables debugging at the level of the application domain. The Moldable Debugger is adapted to a domain by creating and combining domain-specific debugging operations with domain-specific debugging views, and adapts itself to a domain by selecting, at run time, appropriate debugging operations and views. To ensure the proposed model has practical applicability (i.e., can be used in practice to build real debuggers), we discuss, from both a performance and usability point of view, three implementation strategies. We further motivate the need for domain-specific debugging, identify a set of key requirements and show how our approach improves debugging by adapting the debugger to several domains.

Introduction

Debugging is an integral activity of the software development process, consisting in localizing, understanding, and fixing software bugs, with the goal of making software systems behave as expected. Nevertheless, despite its importance, debugging is a laborious, costly and time-consuming activity. Together with testing, debugging can take a significant part of the effort required to ensure the correct functioning of a software system [START_REF] Vessey | Expertise in debugging computer programs: An analysis of the content of verbal protocols[END_REF]. Using inadequate infrastructures for performing these activities can further increase this effort [START_REF] Tassey | The economic impacts of inadequate infrastructure for software testing[END_REF].

Given the pervasiveness of debugging during software maintenance and evolution, numerous debugging techniques have been proposed (e.g., remote debugging, omniscient debugging [START_REF] Pothier | Scalable omniscient debugging[END_REF], post-mortem debugging [START_REF] Yuan | Sherlog: Error diagnosis by connecting clues from run-time logs[END_REF] [5], delta debugging [START_REF] Zeller | Yesterday, my program worked. Today, it does not. Why?[END_REF] -to name a few), each with its own constraints and benefits. These techniques rely on a wide set of tools to locate, extract and analyze data about the run-time behaviour of software systems.

Among the multitude of debugging tools, debuggers are an essential category. If loggers [7] or profilers [START_REF] Ressia | Modeling domain-specific profilers[END_REF] record run-time data presented to developers post-mortem, debuggers enable developers to directly observe the run-time behavior of software and elicit run-time information [START_REF] Roehm | How do professional developers comprehend software?[END_REF][START_REF] Murphy | How are Java software developers using the Eclipse IDE?[END_REF]. In test-driven development the debugger is used as a development tool given that it provides direct access to the running system [START_REF] Beck | Test Driven Development: By Example[END_REF]. This makes the debugger a crucial tool in any programming environment. Nevertheless, there is an abstraction gap between the way in which developers reason about object-oriented applications, and the way in which they debug them.

On the one hand, object-oriented applications use objects to capture and express a model of the application domain. Developers reason about and formulate questions using concepts and abstractions from that domain model. This fosters program comprehension as domain concepts play an important role in software development [START_REF] Littman | Mental models and software maintenance[END_REF][START_REF] Rajlich | The role of concepts in program comprehension[END_REF].

On the other hand, classical debuggers focusing on generic stack-based operations, line breakpoints, and generic user interfaces do not allow developers to rely on domain concepts when debugging object-oriented applications. Furthermore, classical debuggers are less useful when the root of the cause of a bug is far away from its manifestation [START_REF] Zeller | Why Programs Fail: A Guide to Systematic Debugging[END_REF]. Raising the level of abstraction of a debugger by offering object-oriented debugging idioms [START_REF] Ressia | Object-centric debugging[END_REF] solves only part of the problem, as these debuggers cannot capture domain concepts constructed on top of object-oriented programming idioms. Other approaches raise the level of abstraction in different ways: back-in-time debugging, for example, allows one to inspect previous program states and step backwards in the control flow [START_REF] Lienhard | Practical object-oriented back-in-time debugging[END_REF].

Not offering a one-to-one mapping between developer questions and debugging support forces developers to refine their high-level questions into low-level ones and mentally piece together information from various sources. For example, when developing a parser, one common action is to step through the execution until parsing reaches a certain position in the input stream. However, as it has no knowledge of parsing and stream manipulation, a generic debugger requires developers to manipulate low-level concepts like message sends or variable accesses. This abstraction gap leads to an ineffective and error-prone effort [START_REF] Sillito | Asking and answering questions during a programming change task[END_REF].

Creating a debugger that works at the level of abstraction of a given object-oriented application can eliminate the abstraction gap. This can be achieved by:

• automatically generating a debugger based on a meta-model of the application;

• providing developers with a toolset for constructing debuggers for their application.

Automatically generating a debugger requires a specification of the meta-model. The challenge for this approach is to have a meta-model at the right level of detail. Too many or too few details can lead to debuggers with the wrong sets of features. Enabling developers to construct debuggers can lead to debuggers that have only the necessary features. The challenge here is for the toolset to make it possible to create expressive and performant debuggers with low effort.

The first approach has been applied successfully for language workbenches where the domain model of an application is expressed using external domain-specific languages that have a grammar definition [START_REF] Wu | Grammar-driven generation of domain-specific language debuggers[END_REF][START_REF] Lindeman | Declaratively defining domain-specific language debuggers[END_REF][START_REF] Kolomvatsos | Debugging applications created by a domain specific language: The IPAC case[END_REF], as well as in the case of domain-specific modelling languages that have an explicit specification of the metamodel [START_REF] Blunk | Modelling a debugger for an imperative voice control language[END_REF]. Nevertheless, object-oriented programming already has a meta-model in terms of objects and their interactions: object-oriented applications provide an instantiation of the meta-model that expresses domain abstractions through concrete objects and object interactions [START_REF] Rumbaugh | Unified Modeling Language Reference Manual, The (2nd Edition)[END_REF]. To improve development and evolution, these object models take advantage of internal DSLs [START_REF] Fowler | Domain-Specific Languages[END_REF] (e.g., APIs) instead of encoding domain concepts through external DSLs that require a grammar specification.

One can provide an additional specification of an object model from which a debugger can be generated. Nevertheless, non-trivial object-oriented applications contain rich object models [START_REF] Renggli | Embedding languages without breaking tools[END_REF], which can introduce significant overhead in creating and maintaining an extra specification. One can also argue that important concepts from object-oriented applications should be encoded using external DSLs that have an explicit grammar (i.e., meta-model). This, however, does not acknowledge that object-oriented programming already provides the meta-model. In this paper we investigate an alternative solution: we propose a framework for enabling developers to create domain-specific debuggers for their object-oriented applications directly on the existing object model.

When looking at a debugger, there exist two main approaches to address, at the application level, the gap between the debugging needs and debugging support:

• enable developers to create domain-specific debugging operations for stepping through the execution, setting breakpoints, checking invariants [START_REF] Olsson | A dataflow approach to event-based debugging[END_REF][START_REF] Marceau | The design and implementation of a dataflow language for scriptable debugging[END_REF][START_REF] Khoo | Expositor: scriptable time-travel debugging with first-class traces[END_REF] and querying stack-related information [START_REF] Lencevicius | Query-based debugging of object-oriented programs[END_REF][START_REF] Potanin | Snapshot query-based debugging[END_REF][START_REF] Martin | Finding application errors and security flaws using PQL: a program query language[END_REF][START_REF] Ducasse | Object-oriented legacy system trace-based logic testing[END_REF];

• provide debuggers with domain-specific user interfaces that do not necessarily have a predefined content or a fixed layout [START_REF] Deline | Debugger canvas: industrial experience with the code bubbles paradigm[END_REF].

Each of these directions addresses individual debugging problems (i.e., interacting with the runtime at the right level of abstraction and displaying data relevant for the application domain), however until now there does not exist one comprehensive approach to tackle the overall debugging puzzle. We propose an approach that incorporates both of these directions in one coherent model. We start from the realization that the most basic feature of a debugger model is to enable the customization of all aspects, and we design a debugging model around this principle. We call our approach the Moldable Debugger.

The Moldable Debugger decomposes a domain-specific debugger into a domain-specific extension and an activation predicate. The domain-specific extension customizes the user interface and the operations of the debugger, while the activation predicate captures the state of the running program in which that domain-specific extension is applicable. In a nutshell, the Moldable Debugger model allows developers to mold the functionality of the debugger to their own domains by creating domain-specific extensions. Then, at run time, the Moldable Debugger adapts to the current domain by using activation predicates to select appropriate extensions. A domain-specific extension consists of (i) a set of domain-specific debugging operations and (ii) a domain-specific debugging view, both built on top of (iii) a debugging session. The debugging session abstracts the low-level details of a domain. Domain-specific operations reify debugging operations as objects that control the execution of a program by creating and combining debugging events. We model debugging events as objects that encapsulate a predicate over the state of the running program (e.g., method call, attribute mutation) [START_REF] Auguston | A framework for automatic debugging[END_REF]. A domain-specific debugging view consists of a set of graphical widgets that offer debugging information. Each widget locates and loads, at run-time, relevant domain-specific operations using an annotation-based approach.

To validate our model and show that it has practical applicability, we implemented it in Pharo [START_REF]Pharo Programming Language[END_REF], a modern Smalltalk environment and use it to create multiple real-world debuggers. The Moldable Debugger implementation is written in less than 2000 lines of code. We have instantiated it for several distinct domains and each time the implementation required between 200-600 lines of code. We consider that its small size makes it easy to understand, and makes the adaptation of the debugger to specific domains an affordable activity. We further explore three approaches for controlling the execution of the debugged program, approaches applicable depending on the particular aspects of the target domain. This article extends our previous work [START_REF] Chiş | The Moldable Debugger: A framework for developing domain-specific debuggers[END_REF] as follows: (i) we present a more in-depth description of the Moldable Debugger model, (ii) we introduce two new examples of domain-specific debuggers created using the Moldable Debugger model, (iii) we provide a thorough discussion of related work, and (iv) we discuss three approaches for implementing the Moldable Debugger model.

The overall contributions of this paper are as follows:

• Identifying and discussing requirements that an infrastructure for developing domain-specific debuggers should support;

• Discussing the Moldable Debugger, a model for creating and working with domain-specific debuggers that integrates domain-specific debugging operations with domain-specific user interfaces;

• Examples illustrating the advantages of the Moldable Debugger model over generic debuggers;

• A prototype implementation of the Moldable Debugger model together with a discussion of three different approaches for implementing domain-specific debugging operations.

Motivation

Debuggers are comprehension tools. Despite their importance, most debuggers only provide low-level operations that do not capture user intent and standard user interfaces that only display generic information. These issues can be addressed if developers are able to create domain-specific debuggers adapted to their domain concepts. Domain-specific debuggers can provide features at a higher level of abstraction that match the domain model of software applications.

In this section we establish and motivate four requirements that an infrastructure for developing domainspecific debuggers should support, namely: domain-specific user interfaces, domain-specific debugging operations, automatic discovery and dynamic switching.

Domain-specific user interfaces

User interfaces of software development tools tend to provide large quantities of information, especially as the size of systems increases. This, in turn, increases the navigation effort of identifying the information relevant for a given task. While some of this effort is unavoidable, part of it is simply overhead caused by how information is organized on screen [START_REF] Ko | An exploratory study of how developers seek, relate, and collect relevant information during software maintenance tasks[END_REF].

Consider a unit test with a failing equality assertion. In this case, the only information required by the developer is the difference between the expected and the actual value. However, finding the exact difference in non-trivial values can be daunting and can require multiple interactions such as finding the place in the stack where both variables are accessible, and opening separate inspectors for each values. A better approach, if a developer opens a debugger when a test fails, is to show a diff view on the two values directly in the debugger when such an assertion exception occurs, without requiring any further action.

This shows that user interfaces that extract and highlight domain-specific information have the power to reduce the overall effort of code understanding [START_REF] Kersten | Mylar: a degree-of-interest model for ides[END_REF]. However, today's debuggers tend to provide generic user interfaces that cannot emphasize what is important in application domains. To address this concern, an infrastructure for developing domain-specific debuggers should:

• allow domain-specific debuggers to have domain-specific user interfaces displaying information relevant for their particular domains;

• support the fast prototyping of domain-specific user interfaces for debugging.

While other approaches, like deet [START_REF] Hanson | A simple and extensible graphical debugger[END_REF] and Debugger Canvas [START_REF] Deline | Debugger canvas: industrial experience with the code bubbles paradigm[END_REF], support domain-specific user interfaces for different domains, they do not offer an easy and rapid way to develop such domain-specific user interfaces.

Domain-specific debugging operations

Debugging can be a laborious activity requiring much manual and repetitive work. On the one hand, debuggers support language-level operations, while developers think in terms of domain abstractions. As a consequence, developers need to mentally construct high-level abstractions on top of language constructs, which can be time-consuming. On the other hand, debuggers rarely provide support for identifying and navigating through those high-level abstractions. This leads to repetitive tasks that increase debugging time.

Consider a framework for synchronous message passing. One common use case in applications using it is the delivery of a message to a list of subscribers. When debugging this use case, a developer might need to step to when the current message is delivered to the next subscriber. One solution is to manually step through the execution until the desired code location is reached. Another consists in identifying the code location beforehand, setting a breakpoint there and resuming execution. In both cases the developer has to manually perform a series of actions each time she wants to execute this high-level operation.

A predefined set of debugging operations cannot anticipate and capture all relevant situations. Furthermore, depending on the domain different debugging operations are of interest. Thus, an infrastructure for developing domain-specific debuggers should:

• Support the creation of domain-specific debugging operations that allow developers to express and automate high-level abstractions from application domains (e.g., creating domain-specific breakpoints, building and checking invariants, altering the state of the running system). Since developers view debugging as an event-oriented process, the underlying mechanism should allow developers to treat the running program as a generator of events, where an event corresponds to the occurrence of a particular action during the program's execution, like: method entry, attribute access, attribute write or memory access.

• Group together those debugging operations that are relevant for a domain and only make them available to developers when they encounter that domain.

This idea of having customizable or programmable debugging operations that view debugging as an eventoriented activity has been supported in related works [START_REF] Olsson | A dataflow approach to event-based debugging[END_REF][START_REF] Marceau | The design and implementation of a dataflow language for scriptable debugging[END_REF][START_REF] Khoo | Expositor: scriptable time-travel debugging with first-class traces[END_REF][START_REF] Hanson | A simple and extensible graphical debugger[END_REF]. Mainstream debuggers like GDB have, to some extent, also incorporated it. We also consider that debugging operations should be grouped based on the domain and only usable when working with that domain.

Automatic discovery

Based on an observational study of 28 professional developers Roehm et al. report that none of them used a dedicated program comprehension tool; some were not aware of standard features provided by their IDE [START_REF] Roehm | How do professional developers comprehend software?[END_REF]. Another study revealed that despite their usefulness and long lasting presence in IDEs, refactoring tools are heavily underused [START_REF] Murphy-Hill | How we refactor, and how we know it[END_REF].

In the same way, developers need help to discover domain-specific debuggers during debugging. For example, if while stepping through the execution of a program a developer reaches a parser, the environment should facilitate the discovery of a domain-specific debugger that can be used in that context; if later the execution of the parser completes and the program continues with the propagation of an event, the environment should inform the developer that the current domain-specific debugger is no longer useful and that a better one exists. This way, the burden of finding appropriate domain-specific debuggers and determining when they are applicable does not fall on developers.

Recommender systems typically address the problem of locating useful software tools/commands by recording and mining usage histories of software tools [START_REF] Murphy-Hill | Improving software developers' fluency by recommending development environment commands[END_REF] (i.e., what tools developers used as well as how they used them). This requires, at least, some usage history information. To eliminate this need an infrastructure for developing domain-specific debuggers should allow each domain-specific debugger to encapsulate the situations/domains in which it is applicable.

Dynamic switching

Even with just two different types of debuggers, DeLine et al. noticed that users needed to switch between them at run time [START_REF] Deline | Debugger canvas: industrial experience with the code bubbles paradigm[END_REF]. This happened as users did not know in advance in what situation they would find themselves in during debugging. Thus, they often did not start with the appropriate one.

Furthermore, even if one starts with the right domain-specific debugger, during debugging situations can arise requiring a different one. For example, the following scenario can occur: (i) while investigating how an event is propagated through the application (ii) a developer discovers that it is used to trigger a script constructing a GUI, and later learns that (iii) the script uses a parser to read the content of a file and populate the GUI. At each step a different domain-specific debugger can be used. For this to be feasible, domain-specific debuggers should be switchable at debug time without having to restart the application.

Summary

Generic debuggers focusing on low-level programming constructs, while universally applicable, cannot efficiently answer domain-specific questions, as they make it difficult for developers to take advantage of domain concepts. Domain-specific debuggers aware of the application domain can provide direct answers. We advocate that a debugging infrastructure for developing domain-specific debuggers should support the four aforementioned requirements (domain-specific user interfaces, domain-specific debugging operations, automatic discovery and dynamic switching).

A closer look at the "Moldable Debugger" model

The Moldable Debugger explicitly supports domain-specific debuggers that can express and answer questions at the application level. A domain-specific debugger consists of a domain-specific extension encapsulating the functionality and an activation predicate encapsulating the situations in which the extension is applicable. This model makes it possible for multiple domain-specific debuggers to coexist at the same time.

To exemplify the ideas behind the proposed solution we will instantiate a domain-specific debugger for working with synchronous events2 . Event-based programming poses debugging challenges as it favors a control flow based on events not supported well by conventional stack-based debuggers.

Modeling domain-specific extensions

A domain-specific extension defines the functionality of a domain-specific debugger using multiple debugging operations and a debugging view. Debugging operations rely on debugging predicates to implement high-level abstractions (e.g., domain-specific breakpoints); the debugging view highlights contextual information. To decouple these components from the low-level details of a domain they are built on top of a debugging session.

A debugging session encapsulates the logic for working with processes and execution contexts (i.e., stack frames). It further implements common stack-based operations like: step into, step over, resume/restart process, etc. Domain-specific debuggers can extend the debugging session to extract and store custom information from the runtime, or provide fine-grained debugging operations. For example, our event-based debugger extends the debugging session to extract and store the current event together with the sender of that event, the receiver of that event, and the announcer that propagated that event.

Debugging predicates detect run-time events. Basic run-time events (e.g., method call, attribute access) are detected using a set of primitive predicates, detailed in Table 1. More complex run-time events are detected using high-level predicates that combine both primitive predicates and other high-level predicates (Figure 1). Both of these types of debugging predicates are modeled as objects whose state does not change after creation. Debugging predicates are related to coupling invariants from data refinement, as coupling invariants are traditionally defined as logical formulas that relate concrete variables to abstract variables [START_REF] Roever | Data Refinement: Model-Oriented Proof Methods and Their Comparison, 1st Edition[END_REF]. Hence, they can detect specific conditions during the execution of a program.

Consider our event-based debugger. This debugger can provide high-level predicates to detect when a sender initiates the delivery of an event, or when the middleware delivers the event to a receiver.

Attribute read

detects when a field of any object of a certain type is accessed Attribute write detects when a field of any object of a certain type is mutated Method call detects when a given method is called on any object of a certain type Message send detects when a specified method is invoked from a given method State check checks a generic condition on the state of the running program (e.g., the identity of an object).

Table 1: Primitive debugging predicates capturing basic events.

Debugging operations can execute the program until a debugging predicate is matched or can perform an action every time a debugging predicate is matched. They are modeled as objects that can accumulate state. They can implement breakpoints, log data, watch fields, change the program's state, detect violations of invariants, etc. In the previous example a debugging operation can be used to stop the execution when an event is delivered to a receiver. Another debugging operation can log all events delivered to a particular receiver without stopping the execution.

At each point during the execution of a program only a single debugging operation can be active. Thus, debugging operations have to be run sequentially. For example, in the events debugger one cannot activate, at the same time, two debugging operations, each detecting when an event of a certain type is sent to a receiver. One can, however, create a single debugging operation that detects when an event of either type is sent to a receiver. This design decision simplifies the implementation of the model, given that two conflicting operations cannot run at the same time. Hence, no conflict resolution mechanism is required.

The Moldable Debugger models a debugging view as a collection of graphical widgets (e.g., stack, code editor, object inspector) arranged using a particular layout. At run time, each widget loads a subset of debugging operations. Determining what operations are loaded by which widgets is done at run time via a lookup mechanism of operation declarations (implemented in practice using annotations). This way, widgets do not depend upon debugging operations, and are able to reload debugging operations dynamically during execution.

Our event-based debugger provides dedicated widgets that display an event together with the sender and the receiver of that event. These widgets load and display the debugging operations for working with synchronous events, like logging all events or placing a breakpoint when an event is delivered to a receiver.

Developers can create domain-specific extensions by: (i) extending the debugging session with additional functionality;

(ii) creating domain-specific debugging predicates and operations;

(iii) specifying a domain-specific debugging view;

(iv) linking debugging operations to graphical widgets;

Combining predicates

We support two boolean operators for combining debugging predicates: and(predicate1, predicate2, ..., predicateN): creates a new predicate that detects a run-time event when all given predicates detected a run-time event at the same time. This only allows for combining attribute read, attribute write, method call and message send with one or more state check predicates. For example, detecting when a method is called on an a given object is done by using a method call predicate together with a state check predicate verifying the identify of the receiver object.

or(predicate1, predicate2, ..., predicateN): creates a new predicate that detects a run-time event when either one of the given predicates detected a run-time event. For example, detecting when any message is sent from a given method is done by using a message send predicate for every message send from the given method.

Given the definition of the and predicate, detecting high-level events that only happen when a sequence of events is detected is not possible. For example, one cannot detect, just by combing debugging predicates, a sequence of method calls on a given object. This operation requires persistent state and can be implemented by a debugging action.

Dynamic Integration

The Moldable Debugger model enables each domain-specific debugger to decide if it can handle or not a debugging situation by defining an activation predicate. Activation predicates capture the state of the running program in which a domain-specific debugger is applicable. While debugging predicates are applied on an execution context, activation predicates are applied on the entire execution stack. For example, the activation predicate of our event-based debugger will check if the execution stack contains an execution context involving an event.

This way, developers do not have to be aware of applicable debuggers a priori. At any point during debugging they can see what domain-specific debuggers are applicable (i.e., their activation predicate matches the current debugging context) and can switch to any of them.

When a domain-specific debugger is no longer appropriate we do not automatically switch to another one. Instead, all domain-specific widgets and operations are disabled. This avoids confronting users with unexpected changes in the user interface if the new debugging view has a radically different layout/content. Nevertheless, for complex user interfaces where many widgets need to be disabled this solution can still lead to unexpected changes, though this is not as radical as replacing the user interface with a different one.

Designing the disabled widgets in a way that does not confuse users could alleviate part of this issue (e.g., by showing a grayed out version of the widget with no interaction possibilities).

To further improve working with multiple domain-specific debuggers we provide two additional concepts:

A debugger-oriented breakpoint is a breakpoint that when reached opens the domain-specific debugger best suited for the current situation. If more than one view is available the developer is asked to choose one.

Debugger-oriented steps are debugging operations that resume execution until a given domain-specific debugger is applicable. They are useful when a developer knows a domain-specific debugger will be used at some point in the future, but is not sure when or where.

Addressing domain-specific debugging problems

To demonstrate that the Moldable Debugger addresses the requirements identified in Section 2 we have instantiated it for six different domains: testing, synchronous events, parsing, internal DSLs, profiling and bytecode interpretation. In this section we detail these instantiations.

Testing with SUnit

SUnit is a framework for creating unit tests [START_REF] Beck | Kent Beck's Guide to Better Smalltalk[END_REF]. The framework provides an assertion to check if a computation results in an expected value. If the assertion fails the developer is presented with a debugger that can be used to compare the obtained value with the expected one. If these values are complex, identifying the difference may be time consuming. A solution is needed to facilitate comparison.

To address this, we developed a domain-specific debugger having the following components:

Session: extracts the expected and the obtained value from the runtime;

View : displays a diff between the textual representation of the two values. The diff view depends on the domain of the data being compared. Figure 2 shows how it can be used to compare two HTTP headers;

Activation predicate: verifies if the execution stack contains a failing equality assertion.

An Announcement-Centric debugger

The Announcements framework from Pharo provides a synchronous notification mechanism between objects based on a registration mechanism and first class announcements (i.e., objects storing all information relevant to particular occurrences of events). Since the control flow for announcements is event-based, it does not match well the stack-based paradigm used by conventional debuggers. For example, Section 2.2 describes a high-level action for delivering an announcement to a list of subscribers. Furthermore, when debugging announcements it is useful to see at the same time both the sender and the receiver of an announcement;

most debuggers only show the receiver.

To address these problems we have created a domain-specific debugger, shown in Figure 3. A previous work discusses in more details the need for such a debugger and looks more closely at the runtime support needed to make the debugger possible [START_REF] Chiş | Towards a moldable debugger[END_REF]. This debugger is instantiated as follows:

Session: extracts from the runtime the announcement, the sender, the receiver and all the other subscriptions triggered by the current announcement;

Predicates:

Detect when the framework initiates the delivery of a subscription: message send (deliver: in Subscrip-tionRegistry >> deliver:to:startingAt: 3 ) Detect when the framework delivers a subscription to an object: method call (aSubscription action selector) on the class of the object and state check verifying the identity of the target object and state check verifying that the message was sent from the announcer holding the given subscription;

Operations:

Step to the delivery of the next subscription;

Step to the delivery of a selected subscription;

View : shows both the sender and the receiver of an announcement, together with all subscriptions served as a result of that announcement;

Activation predicate: verifies if the execution stack contains an execution context initiating the delivery of an announcement.

A debugger for PetitParser

PetitParser is a framework for creating parsers, written in Pharo, that makes it easy to dynamically reuse, compose, transform and extend grammars [START_REF] Renggli | Practical dynamic grammars for dynamic languages[END_REF]. A parser is created by specifying a set of grammar productions in one or more dedicated classes. When a parser is instantiated the grammar productions are used to create a tree of primitive parsers (e.g., choice, sequence, negation); this tree is then used to parse the input.

Whereas most parser generators instantiate a parser by generating code, PetitParser generates a dynamic graph of objects. Nevertheless, the same issues arise as with conventional parser generators: generic debuggers do not provide debugging operations at the level of the input (e.g., set a breakpoint when a certain part of the input is parsed) and of the grammar (e.g., set a breakpoint when a grammar production is exercised). Generic debuggers also do not display the source code of grammar productions nor do they provide easy access to the input being parsed. To overcome these issues, other tools for working with parser generators provide dedicated domain-specific debuggers. For example, ANTLR Studio an IDE for the ANTLR [START_REF]ANTLR -ANother Tool for Language Recognition[END_REF] parser generator provides both breakpoints and views at the level of the grammar [START_REF]ANTLR -Debugging ANTLR grammars using ANTLR Studio[END_REF]. Rebernak et al. also give an example of a dedicated debugger for ANTLR [START_REF] Rebernak | Domain-specific aspect languages for modularising crosscutting concerns in grammars[END_REF].

In the case of PetitParser we have developed a domain-specific debugger by configuring the Moldable Debugger as follows:

Session: extracts from the runtime the parser and the input being parsed;

Predicates:

Detect the usage of any type of parser: method call (parseOn:) predicates combined using or on all subclasses of PPParser that are not abstract and override the method parseOn:;

Detect the usage of any type of production: method call (PPDelegateParser>>parseOn:);

Detect the usage of a specific primitive parser: method call (parseOn:) predicates combined using or on all subclasses of PPParser that represent a primitive parser (e.g., PPRepeatingParser);

Detect the usage of a specific production: method call (PPDelegateParser>>parseOn:) and state check verifying that the receiver object is a parser for the given grammar production;

Detect when a parser fails to match the input: method call (PPFailure class>>message:context:), or method call (PPFailure class>>message:context:at:);

Detect when the position of the input stream changes: attribute write(#position from PPStream) and state check verifying that the attribute value changed;

Detect when the position of the input stream reaches a given value: attribute write(#position from PPStream) and state check verifying that the attribute value is set to a given value; View : The debugging view of the resulting debugger is shown in Figure 4. We can see that now the input being parsed is incorporated into the user interface; to know how much parsing has advanced, the portion that has already been parsed is highlighted. Tabs are used to group six widgets showing different types of data about the current production, like: source code, structure, position in the whole graph of parsers, an example that can be parsed with the production, etc. The structure of the parser (e.g., the Graph view in Figure 4), for example, is generated from the object graph of a parser and can allow developers to navigate a production by clicking on it. The execution stack further highlights those execution contexts that represent a grammar production;

Activation predicate: verifies if the execution stack contains an execution context created when using a parser.

A debugger for Glamour

Glamour is an engine for scripting browsers based on a components and connectors architecture [START_REF] Bunge | Scripting browsers with Glamour[END_REF]. New browsers are created by using an internal domain-specific language (DSL) to specify a set of presentations (graphical widgets) along with a set of transmissions between those presentations, encoding the information flow. Users can attach various conditions to transmissions and alter the information that they propagate. Presentations and transmissions form a model that is then used to generate the actual browser.

The Moldable Debugger relies on Glamour for creating domain-specific views. Thus, during the development of the framework we created a domain-specific debugger to help us understand the creation of a browser:

Session: extracts from the runtime the model of the browser;

Predicates:

Detect the creation of a presentation: message send (glamourValue: in GLMPresentStrategy>>presentations); Detect when a transmission alters the value that it propagates: message send (glamourValue: in GLMTransmission>>value); Detect when the condition of a transmission is checked: message send (glamourValue: in GLMTransmission>>meetsCondition);

Operations:

Step to presentation creation

Step to transmission transformation

Step to transmission condition View : displays the structure of the model in an interactive visualization that is updated as the construction of the model advances (Figure 5);

Activation predicate: verifies if the execution stack contains an execution context that triggers the construction of a browser.

Profiler framework

Spy is a framework for building custom profilers [START_REF] Bergel | Spy: A flexible code profiling framework[END_REF]. Profiling information is obtained by executing dedicated code before or after method executions. This code is inserted into a method by replacing the target method with a new method (i.e., method wrapper [START_REF] Brant | Wrappers to the rescue[END_REF]) that executes the code of the profiler before and after calling the target method. Hence, if a developer needs to debug profiled code she has to manually skip over the code introduced by the profiler to reach the target method. To address this issue, together with the developers of S2py [START_REF]S2py Profiling Framework[END_REF] (the second version of the Spy framework), we created a domain-specific debugger that does not expose developers to profiler code when debugging methods are being profiled:

Session: no changes in the default debugging session are required;

Predicates:

Detect when Spy finished executing profiling code for a method message send (valueWithReceiver:arguments: in S2Method>>run:with:in:)

Operations:

Step into method call ignoring profiled code: debugging action that when stepping into a method profiling code automatically steps over the profiler code and into the code of the original method.

View : has the same widgets as the user interface of the default debugger. However, the stack widget removes all stack frames internal to the Spy framework.

Activation predicate: verifies if the execution stack contains an execution context that starts a Spy profiler.

Stepping through bytecodes

While normally programs are debugged at the source level, building tools like compilers and interpreters requires developers to understand the execution of a program at the bytecode level. However, all debugging actions presented in this section skip over multiple bytecode instructions. For example, step into message send, a debugging action present in most debuggers skips over the bytecode instructions that push method parameters onto the stack. The ability to debug at the bytecode level is especially important when generating or manipulating bytecode directly with bytecode transformation tools. In many cases the resulting bytecode cannot be de-compiled to a textual representation. To address this we developed a debugger for stepping through the execution of a program one bytecode instruction at a time:

Session: customizes the default debugging session to not step over multiple bytecode instructions when performing various initializations;

Predicates: no predicates are required Operations:

Step over one bytecode instruction;

Step into a bytecode instruction representing a message send;

View : shows the method that is currently executed both as a list of bytecodes and a textual representation;

embeds an object inspector that shows the internal stack of the current execution context (Figure 6);

Activation predicate: uses an activation predicate that always returns true.

Summary

PetitParser, Glamour, SUnit, Spy, Announcements framework and bytecode interpretation cover six distinct domains. For each one we instantiated a domain-specific debugger having a contextual debugging view and/or a set of debugging operations capturing abstractions from that domain. This shows the Moldable Debugger framework addresses the first two requirements.

The two remaining requirements, automatic discovery and dynamic switching, are also addressed. At each point during debugging developers can obtain a list of all domain-specific debuggers applicable to their current context. This does not require them either to know in advance all available debuggers, or to know when those debuggers are applicable. Once the right debugger was found developers can switch to it and continue debugging without having to restart the application. For example, one can perform the scenario presented in Section 2.4. The cost of creating these debuggers is discussed in Section 6.1.

Implementation aspects

While the Moldable Debugger model can capture a wide range of debugging problems the actual mechanism for detecting run-time events has a significant impact on the performance and usability of a debugger. In this section we present and discuss, from both a performance and usability point of view, three approaches for detecting run-time events in the execution of a debugged program based on debugging predicates:

(i) step-by-step execution;

(ii) code-centric instrumentation;

(iii) object-centric instrumentation.

Controlling the execution

When an event is detected a breakpoint is triggered, stopping the execution of the debugged program. The breakpoint notifies the active debugging action (i.e., the action that installed the predicate). The debugging action can then perform an operation, resume the execution or wait for a user action.

We use the following debugging actions from the PetitParser debugger as examples in this section:

• Production(aProduction): step until the given grammar production is reached;

• Stream position(anInteger): step until parsing reaches a given position in the input stream.

5.1.1.

Step-by-step execution Approach. Interpret the debugged program one bytecode instruction at a time (i.e., step-by-step execution) and check, after each bytecode instruction, if a debugging predicate matches the current execution context (i.e., stack frame). This approach matches the while-step construct proposed by Crawford et al. [START_REF] Crawford | Semantic issues in the design of languages for debugging[END_REF].

Implementation. Each debugging predicate is transformed to a boolean condition that is applied to the current execution context (i.e., stack frame).

Examples.

• Production(aProduction): (i) check if the current bytecode instruction is the initial instruction of a method; (ii) check if the currently executing method is PPDelegateParser>>parseOn:; (iii) check if the receiver PPDelegateParser object is a parser for the given grammar production;

• Stream position(anInteger): (i) check if the current bytecode instruction pushes a value into an object attribute; (ii) check if the attribute is named #stream and it belongs to an instance of PPStream; (iii) check if the value that will be pushed into the attribute is equal to the given value.

Code-centric instrumentation

Approach. Use basic debugging predicates (i.e., attribute access, attribute write, method call and message send ) to insert instrumentations into the code of the debugged program. State check predicates can then ensure that a breakpoint is triggered only if further conditions hold when the instrumentation is reached (e.g., the target object is equal to a given one). This approach resembles dynamic aspects [START_REF] Bonér | What are the key issues for commercial AOP use: how does AspectWerkz address them?[END_REF] and conditional breakpoints.

Implementation. We rely on two different mechanisms for handling predicates for attributes and methods. We implement attribute access and attribute write predicates using slots [START_REF] Verwaest | Flexible object layouts: enabling lightweight language extensions by intercepting slot access[END_REF]. Slots model instance variables as first-class objects that can generate the code for reading and writting instance variables. We rely on a custom slot that can wrap any existing slot and insert code for triggering a breakpoint before the attribute is read or written in all methods of a given class.

We implement method call and message send by adding meta-links to AST nodes [START_REF] Denker | Sub-method reflection[END_REF]: when compiling an AST node to bytecode, if that AST node has an attached meta-link, that meta-link can generate code to be executed before, after or instead of the code represented by the AST node. We rely on a custom meta-link that inserts code for triggering a breakpoint before the execution of an AST node. We then implement these types of predicates as follows:

• message send : locate in the body of a method all AST nodes that represent a call to the target method; add the custom meta-link only to these AST nodes;

• method call : add the custom meta-link on the root AST node of the target method.

A different strategy for implementing code-centric instrumentations consists in injecting the debugged concern directly into an existing bytecode version of the code using a bytecode engineering library [START_REF] Tanter | Altering Java semantics via bytecode manipulation[END_REF]. The meta-links used have similar properties: code instrumentation happens at runtime and the original code remains unchanged. Direct bytecode manipulation would give a more fined-grained control on the position where and how code inserted into the debugged code. This flexibility is not needed for our debugger and it would come with the cost of having to deal with the complexity of bytecode.

Examples.

• Production(aProduction): instrument the root AST node of PPDelegateParser>>parseOn: to check if the receiver object is a parser for the given grammar production;

• Stream position(anInteger): add instrumentations before all write instructions of the attribute #stream from class PPStream to check if the new value of the attribute is equal with a given value.

Object-centric instrumentation

Approach. Combine a basic predicate (i.e., attribute access, attribute write, method call or message send ) with a state check predicate verifying the identity of the receiver object against a given object (i.e., identity check predicate). Then insert into the debugged program instrumentations only visible to the given object.

Thus, a breakpoint is triggered only when the event captured by the basic predicate has taken place on the given instance. This approach matches the object-centric debugging approach [START_REF] Ressia | Object-centric debugging[END_REF], where debugging actions are specified at the object level (e.g., stop execution when this object receives this message).

Implementation. We insert an instrumentation visible to only a single target object as follows:

• create an anonymous subclass of the target object's class; the anonymous subclass is created dynamically, at debug time, when the underlying dynamic action is executed;

• apply code-centric instrumentation to insert the basic debugging event into the anonymous class; codecentric instrumentation is inserted through code generation and recompilation of the anonymous class at debug time;

• change the class of the target object to the new anonymous class.

As access to a run-time object is necessary this approach can only be used once a debugger is present; it cannot be used to open the initial debugger.

Examples.

• Production(aProduction): (i) locate the PPDelegateParser object that represents the given grammar production; (ii) replace the class of that object with an anonymous one where the method parseOn:

has a method call predicate inserted using a code-centric instrumentation;

• Stream position(anInteger): (i) locate the PPStream object holding the input that is being parsed; (ii) replace the class of that object with an anonymous one where the attribute #position: has an attribute access predicate inserted using a code-centric instrumentation.

Performance

To investigate the performance overhead of the aforementioned approaches we performed a series of microbenchmarks. We performed these benchmarks using the current prototype of the Moldable Debugger4,5 implemented in Pharo, an open-source Smalltalk environment, on an Apple MacBook Pro, 2.7 GHz Intel Core i7 in Pharo 4 with the jitted Pharo VM 6 . We ran each benchmark 5 times and present the average time of these runs in milliseconds. All presented times exclude garbage collection time.

5.2.1.

Step-by-step execution Basic benchmarks. We performed, for each basic predicate, a benchmark checking for an event (i.e., method call/message send/attribute access/attribute/condition over the state) not present in the measured code. Given that a predicate never matches a point in the execution, the boolean condition will be checked for every bytecode instruction, giving us the worst-case overhead of these predicates on the measured code.

The measured code(line 4)7 consists of ten million calls to the method Counter>>#increment (lines 1-2). We selected this method as it has only one message send, attribute access and attribute write, making it possible to use it for all predicates (for method call we instrument the call to increment).

1 Counter>>#increment As expected, this approach introduces a significant overhead of more than three orders of magnitude for all predicates, when the event of interest is not detected (Table 2). The high overhead is due to the step-by-step execution rather than to the actual condition being checked: verifying the identity of an object using a state check predicate (Table 2 -identity check ) has the same overhead as a state check predicate that performs no check (Table 2

-empty state check).

Advanced benchmarks. To determine if this high overhead is present when dealing with real-world code, rather than a constructed example, we performed five more benchmarks presented in Listings 1 -5. In each benchmark we used a method call predicate detecting the execution of a method not called in the measured code. Like in the previous benchmarks this gives us the maximal performance impact for this predicate. We only use one type of basic predicate given that all types of basic predicates exhibit a similar overhead. We obtained different results (Table 3) than in the previous set of benchmarks ranging from an overhead of only 2.6× to an overhead of 1120×. These diverging results can be explained by looking at one particular aspect of the measured code: the time spent in method calls that are implemented directly by the VM (i.e., primitive operations) and thus cannot by executed in a step-by-step manner by a bytecode interpreter.

Predicate

For example, on the one hand, when computing factorial most time is spent doing additions, an operation implemented directly by the VM. Merge sort, on the other hand, spends little time in primitives; thus exhibits similar worst-case overhead to the example code from the previous benchmarks.

Code-centric instrumentation

This approach does not introduce any runtime overhead when using basic predicates to detect attribute 575 reads/writes, message sends and method calls. The overhead comes from combining these predicates with state check predicates, and from the actual implementation mechanism used to check the condition. Given that we use two approaches for instrumenting code (i.e., slots, AST annotations) we performed measurements that combine attribute access and method call predicates with state check predicates.

Basic benchmarks. We first combine the aforementioned predicates with an identity check predicate. For each situation we perform a benchmark on only the operation we are interested in (i.e., attribute writelines 20-21, method call to returnOne -lines [START_REF] Rumbaugh | Unified Modeling Language Reference Manual, The (2nd Edition)[END_REF][START_REF] Fowler | Domain-Specific Languages[END_REF] and on the #increment method used in the previous section. We execute each method ten million times on one object and use an identity check predicate that never detects an event in the measured code (i.e., checks for another object). As seen from Table 4 the overhead is significantly lower than the one introduced by step-by-step execution. Regardless of the predicate, the highest overhead is obtained for the methods initializeWithOne and returnOne where, given that the methods have almost no functionality, any extra instrumentation increases execution time. The overhead for the increment method is lower as this method performs more operations than the previous two. Nevertheless, the method call predicate has an overhead six times higher than attribute write predicate. While for both implementations we reify the current stack frame before checking any associated condition, Reflectivity, the framework used for code instrumentation has an expensive mechanism for detecting recursive calls from meta-links (i.e., detect when a meta-link is added in code called from the meta-link). Repeating these measurements when the basic predicates are combined with five identity check predicates results in only slightly higher overheads for all benchmarks. This indicates that most of the overhead comes from reifying the execution context every time a condition needs to be checked.

Based on the previous observation a further improvement can be done when combining an attribute access/attribute write predicate with a state check predicate that only accesses the new and old value of the instance variable: given that we use slots for instrumenting attributes accesses/writes we can directly get the new and old values of the attribute from the slot without reifying the current stack frame. This leads to a performance overhead just x8 when changes in the value of an attribute.

A further improvement in performance can be achieved by removing altogether the need for reifying the current stack frame. A method call predicate combined with an identity check predicate can directly insert in the code of the target method a condition comparing self (this in Java) with a given object. Our current prototype does not support these kinds of instrumentations. Nevertheless, the performance overhead required to reify the stack frame is small enough to have practical applicability. This approach further allows developers to test any kind of property of the stack frame (e.g., the position of the program counter).

Advanced benchmarks. We performed four benchmarks on the following domain-specific actions presented in Section 4: Production(aProduction), Stream position(anInteger), Parser(aParserClass) and Subscription(aSubscription). For the first three we used the code from Listing 4, while for the last one we used the code from Listing 5. For each debugging action we look for an event not present in the measured code (e.g., a production not present in the parser).

For all debugging actions we get a runtime overhead lower than the one from the basic benchmarks ranging from 1.6x to 27.7x (Table 5). This is expected because in this case the event that triggers a breakpoint is encountered far less often. The debugging action Parser(aParserClass) has the largest overhead as it introduces a high number of instrumentations. In the code on the left, detecting when targetObject is initialized with value 2 has the same overhead as using code-centric instrumentations given that the condition must be checked on every write of the counter attribute (as seen in the previous section verifying the identity of one or more objects incurs a similar overhead, given that what takes the most time is reifying the execution context).

In the code on the right, the runtime overhead when checking that one object from the collection is initialized with 2 is negligible, as the condition is checked only once. Installing the predicate on every object will lead to a similar runtime overhead as in the previous case, given that the condition will be checked ten million times.

Advanced benchmarks. The same observations from Basic benchmarks apply. On the one hand, in the action Stream position(anInteger), detecting when the stream has reached a certain position using an object-centric instrumentation has the same overhead as a code-centric instrumentation given that there is a single stream object shared by all the parsers. On the other hand, applying the action Subscription(aSubscription) on the code from Listing 5 has a negligible overhead as each announcement is delivered to a different object.

Usability

Step-by-step execution. The main advantage of this approach is that it is simple to understand and implement, and it does not alter the source of the debugged program. However, it can slow down the debugged program considerably to the point where it is no longer usable. Despite this shortcoming it can be useful for debugging actions that need to skip over a small number of bytecode instructions. For example, we use this approach to implement the action Step to next subscription in the Announcements debugger: we only need to skip over several hundred bytecode instructions internal to the Announcements framework.

Code-centric instrumentation. This approach has a much lower performance overhead than step-by-step execution that makes it practically applicable in most situations. While the current overhead is low, it can still prove inconvenient when using complex predicates or when stepping over code that already takes a significant amount of time. For example, we do not use this solution in the Announcements debugger as the Announcements framework is heavily used by Pharo IDE, and any overhead will apply to the entire IDE.

Object-centric instrumentation. While it imposes no performance overhead, this approach does not work for code that depends on the actual class of the instrumented object. It further requires access to an object beforehand, which is not always possible. We use this solution in the Announcements and PetitParser debuggers; however, in both cases we only instrument objects internal to these frameworks.

Discussion. Even if not practically applicable in most situations we used debugging actions based on stepby-step execution to implement the initial version of all our domain-specific debuggers. This allowed us to quickly prototype and refine the interface and the functionality of those debuggers. Later on, whenever performance became a problem we moved to actions based on code-centric instrumentation. We then only changed these actions to object-centric instrumentation in very specific situations where we could take advantage of particular aspects of a framework (e.g., PetitParser uses a single PPContext object that is passed to all the parse methods; the Announcements framework creates an internal subscription object each time a subscriber registers with an announcer).

Depending on the particular aspects of a domain, not all three approaches are applicable. Table 6 indicates what approaches could be used for the example debuggers from Section 4 (Glamour is a prototypebased framework that relies on copying objects; Spy already instruments the debugged code).

Step-by-step execution

Code-centric instrumentations

Object-centric instrumentations Announcements Petit Parser Glamour Spy Bytecode Table 6: Feasible approaches for implementing debugging actions for the example debuggers from Section 4.

Note that the performance penalty is present only when using the custom debugger, and not when using the regular one.

The Moldable Debugger in other languages

The current prototype of the Moldable Debugger is implemented in Pharo. It can be ported to other languages as long as:

• they provide a debugging infrastructure that supports custom extensions/plugins for controlling the execution of a target program;

• there exists a way to rapidly construct user interfaces for debuggers, either through domain-specific languages or UI builders.

For example, one could implement the framework in Java. Domain-specific debugging operations can be implemented on top of the Java Debugging Interface (JDI) or by using aspects. JDI is a good candidate as it provides explicit control over the execution of a virtual machine and introspective access to its state. Aspect-Oriented Programming [START_REF] Kiczales | Aspect-oriented programming[END_REF] can implement debugging actions by instrumenting only the code locations of interest. Dynamic aspects (e.g., AspectWerkz [START_REF] Bonér | What are the key issues for commercial AOP use: how does AspectWerkz address them?[END_REF]) can further scope code instrumentation at the debugger level. Last but not least, domain-specific views can be obtained by leveraging the functionality of IDEs, like perspectives in the Eclipse IDE.

Discussion

The cost of creating new debuggers

The four presented domain-specific debuggers were created starting from a model consisting of 1500 lines of code. Table 7 shows, for each debugger, how many lines of code were needed for the debugging view, the debugging actions, and the debugging session. Regarding the view column, custom debuggers extend and customize the view of the default debugger; hence, the view of default debugger is twice the size of any other view, as it provides the common functionality needed in a debugging interface. In general, lines of code (LOC) must be considered with caution when measuring complexity and development effort. The metric does not necessarily indicate the time needed to write those lines. Nevertheless, it gives a good indication of the small size of these domain-specific debuggers. This small size makes the construction cost affordable. Similar conclusions can be derived from the work of Kosar et al. that shows that with the right setup it is possible to construct a domain-specific debugger for a modelling language with relatively low costs [START_REF] Kosar | Debugging measurement systems using a domain-specific modeling language[END_REF]. Hanson and Korn further show that a useful debugger for C can be written in under 2500 lines of code, one order of magnitude smaller than gdb [START_REF] Hanson | A simple and extensible graphical debugger[END_REF].

The availability of a moldable infrastructure opens new possibilities:

(i) the developers of a library or framework can create and ship a dedicated debugger together with the code, to help users debug that framework or library. For example, the developers of PetitParser and Glamour can build custom debuggers themselves and ship them together with the frameworks;

(ii) developers can extend the debugger for their own applications, during the development process, to help them solve bugs or better understand the application. Smith et al. observed that developers take the initiative and build tools to solve problems they face during software development, even if they rarely share those tools [START_REF] Smith | Build it yourself! homegrown tools in a large software company[END_REF].

Applicability

Section 4 shows that the Moldable Debugger can cover a wide range of application domains. While Section 4 just gives particular examples, we consider the Moldable Debugger to be applicable for most types of application domains that build upon an object-oriented model. For example, one could apply the proposed solution to AmbientTalk [START_REF] Cutsem | AmbientTalk: programming responsive mobile peer-to-peer applications with actors[END_REF], an actor-based distributed programming language by extending the Moldable Debugger with support for actor-based concurrency, though it could require significant effort.

The applicability of the Moldable Debugger, nevertheless, has its limits. An edge case is Monaco, a domain-specific language for reactive systems with imperative programming notation [START_REF] Prähofer | Monaco-a domain-specific language solution for reactive process control programming with hierarchical components[END_REF]. While Monaco has a model based on hierarchical components that could be accommodated by the Moldable Debugger, the main goal of Monaco is to develop programs for control systems. As running and debugging programs on live control systems is not a feasible option, simulators, rather then debuggers provide better support for reasoning about theses types of program. A case where the Moldable Debugger would not be applicable is SymGridPar2, a language for parallel symbolic computation on a large number of cores [START_REF] Maier | Reliable scalable symbolic computation: The design of SymGridPar2, Computer Languages[END_REF]. On the one hand SymGridPar2 features a functional programming style. On the other hand it is designed for programs that will run in parallel on tens of thousand of cores. The run-time overhead added by a debugger can significantly influence the behaviour of the code. Logging frameworks provide better alternatives as they allow developers to collect information at run time with a very low overhead and analyze it postmortem with more costly analyses.

IDE Integration

Studies of software developers revealed that they use standalone tools alongside an IDE, even when their IDE has the required features, as they often cannot find those features [START_REF] Roehm | How do professional developers comprehend software?[END_REF]. Furthermore, developers also complain about loose integration of tools that forces them to look for relevant information in multiple places [START_REF] Maalej | Task-first or context-first? Tool integration revisited[END_REF]. To avoid these problems the Moldable Debugger framework is integrated into the Pharo IDE and essentially replaces the existing debugger. On the one hand, Pharo made it easy to integrate the Moldable Debugger due to its powerful introspection support. For example, the entire run-time stack can be reified on demand and the class of an object can be changed dynamically at run time. Pharo further incorporates support for slots and behaviour reflection through AST annotations. On the other hand, due to the highly dynamic nature of Pharo/Smalltalk not all entry points for the debugger were clear, at the beginning.

The Moldable Debugger along with the domain-specific debuggers presented in Section 4 are also integrated into Moose9 , a platform for data and software analysis [START_REF] Nierstrasz | The story of Moose: an agile reengineering environment[END_REF]. Despite the fact that the performance of the current implementation can be significantly improved, these domain-specific debuggers are usable and reduce debugging time. We are using the domain-specific debugger for PetitParser on a daily basis, as the default debugger significantly increases debugging time.

Pitfalls and limitations

Depending on the application domain and the actual debugger a developer wants to build, deeper knowledge about program execution may be needed. Hence, depending on the requirements, creating a domainspecific debugger is not an activity suitable for developers lacking this kind of knowledge.

Domain concepts, as well as the code that implements them can change. Given that we propose a manual approach for constructing domain-specific debuggers, these debuggers have to be manually kept in sync with both the domain concepts from an application and their actual implementation. Furthermore, bugs in the code of an application can introduce wrong behaviour in the debugger. For instance, a debugging predicate may not detect the desired run-time event due to remaining bugs in the code that needs to be debugged. Based on our experience in developing several domain-specific debuggers, a developer with domain knowledge can, with a low effort, detect these situations while developing a domain-specific debugger. The main issue consists in making sure that new changes in an application do not break assumptions made when developing a debugger. Currently, together with each debugger we also created smoke tests that exercise the main functionality of that debugger. This gives a minimum safety-net for detecting changes that break the debugger. A wider range of tests that exercises different usage scenarios could detect significantly more problematic changes. While writing these tests by hand, for each debugger, could be an option, we also see automatic test generation based on more meta-information as a possible solution, though we currently did not investigate this alternative.

Open questions

As software systems evolve domain-specific debuggers written for those systems must also evolve. This raises further research questions like: "What changes in the application will lead to changes in the debugger?" or "How can the debugger be kept in sync with the application?". For example, introducing code instrumentation or destructive data reading (as in a stream) can lead to significant changes in an existing debugger.

In this context, a more high-level question is "What makes an application debuggable?". By this we mean what characteristics of an application ease, or exacerbate the creation of debuggers or, more generally, what characteristics affect debugging. To draw an analogy, in the field of software testing high coupling makes the creation of unit tests difficult (by increasing the number of dependencies that need to be taken into account) and thus decreases the testability of a software system.

Related Work

There exists a wide body of research addressing debugging from various perspectives. In this section we give an overview of several perspectives related to the Moldable Debugger model.

Software logging

While debuggers aim to support a direct interaction with the run-time state of an application, logging frameworks just record dynamic information about the execution of a program. One important challenge with logging frameworks, related to the current work, is how to capture the right information about the runtime [START_REF] Oliner | Advances and challenges in log analysis[END_REF]. Given the large diversity of information that could be relevant about a running application, like the Moldable Debugger, many frameworks for logging run-time information allow developer to customize the logging infrastructure to fit their needs [7,[START_REF] Erlingsson | Extensible distributed tracing from kernels to clusters[END_REF][START_REF] Ressia | Modeling domain-specific profilers[END_REF]]. MetaSpy, for example, makes is possible to easily create domain specific-profilers [START_REF] Ressia | Modeling domain-specific profilers[END_REF].

In the context of wireless sensor networks Cao et al. propose declarative tracepoints, a debugging system that allows users to insert action-associated checkpoints using a SQL-like language. Like activation predicates it allows users to detect and log run-time events using condition predicates over the state of the program [START_REF] Cao | Declarative tracepoints: A programmable and application independent debugging system for wireless sensor networks[END_REF].

Specifying domain-specific operations

There is a wide body of research in programmable/scriptable debugging allowing developers to automate debugging tasks by creating high-level abstractions. MzTake [START_REF] Marceau | The design and implementation of a dataflow language for scriptable debugging[END_REF] is a scriptable debugger enabling developers to automate debugging tasks, inspired by functional reactive programming. MzTake treats a running program as a stream of run-time events that can be analyzed using operators, like map and filter ; streams can be combined to form new streams. For example, one can create a stream in which a new value is added every time a selected method is called from the debugged program. Selecting only method calls performed on objects that are in a certain state is achieved using the filter operator; this operator creates a new stream that contains only the method call events that matched the filter's condition. Unlike MzTake we propose an approach for detecting run-time events based on object-oriented constructs: run-time events are specified by combining predicate objects, instead of combining streams. A debugging action can use then a predicate to detect a run-time event (e.g., method call on a given object) and put the event in a stream.

Coca [START_REF] Ducassé | Coca: An automated debugger for C[END_REF] is an automated debugger for C using Prolog predicates to search for events of interest over program state. Events capture various language constructs (e.g., function, return, break, continue, goto) and are modelled as C structures; a sequence of events is grouped in a trace. Developers write, using Prolog, queries that search for an event matching a pattern in a trace. To perform a query a developer has to provide an event pattern and call a primitive operation for performing the actual search. The event patterns consists of any combination of 3-tuples of the form '<attributename> <operation> <attribute-value>' connected with and, or or not. In our approach we express high-level run-time events by combining objects (i.e., debugging predicates) instead of combining tuples. We further use predicates as a specification of run-time events and employ different implementations to detect those events at run-time.

Dalek [START_REF] Olsson | A dataflow approach to event-based debugging[END_REF] is a C debugger employing a dataflow approach for debugging sequential programs: developers create high-level events by combining primitive events and other high-level events. Developers enter breakpoints into the debugged code to generate primitive events. A high-level event is created by specifying in the definition of that event what events activate that event; when an event is triggered all other events that depend on it are also triggered. High-level events can maintain state and execute code when triggered (e.g., print, test invariants). Thus, high-level events map to debugging actions in our approach. However, we do not require developers to explicitly trigger primitive events from the debugged code; developers provide a specification of the run-time event using debugging predicates, outside of the debugged program's code.

Auguston et al. present a framework that uses declarative specifications of debugging actions over event traces to monitor the execution of a program [START_REF] Auguston | A framework for automatic debugging[END_REF]. Several types of run-time events, corresponding to various actions encountered in the debugged program, are generated directly by the virtual machine. Events are grouped together in traces that conform to an event grammar, defining the valid traces of events. An execution monitor loads a target program, executes it, obtains a trace of run-time events from the program and performs various computations over the event trace (e.g., check invariants, profile). We do not have an explicit concept of monitor in our approach and do not directly provide operations for manipulating event traces. Our model only associates run-time events (predicates) with various operations. Event traces can be implemented on top of this model, by having debugging actions that store and manipulate events.

Expositor [START_REF] Khoo | Expositor: scriptable time-travel debugging with first-class traces[END_REF] is a scriptable time-travel debugger that can check temporal properties of an execution: it views program traces as immutable lists of time-annotated program state snapshots and uses an efficient data structure to manage them. Acid [START_REF] Winterbottom | ACID: A debugger built from a language[END_REF] makes it possible to write debugging operations, like breakpoints and step instructions, in a language designed for debugging that reifies program state as variables.

The aforementioned approaches focus on improving debugging by enabling developers to create commands, breakpoints or queries at a higher level of abstraction. Nevertheless, while they encapsulate high-level abstractions into scripts, programs or files, developers have to manually find proper high-level abstractions for a given debugging context. We propose an approach for automatically detecting relevant high-level abstractions (e.g., debugging actions) based on the current debugging context. Furthermore, only some of the aforementioned approaches incorporate at least ad hoc possibilities of visually exploring data by using features from the host platform. We propose a domain-specific view for each domain-specific debugger that displays and groups relevant widgets for the current debugging context.

Object-centric debugging [START_REF] Ressia | Object-centric debugging[END_REF] proposes a new way to perform debugging operations by focusing on objects instead of the execution stack; while it increases the level of abstraction of debugging actions to objectoriented idioms, the approach does not enable developers to create and combine debugging actions to capture domain concepts instead of just object-oriented idioms. Reverse watchpoints use the concept of position to automatically find the last time a target variable was written and move control flow to that point [START_REF] Maruyama | Debugging with reverse watchpoint[END_REF]. Whyline is a debugging tool that allows developer to ask and answer Why and Why Not questions about program behavior [START_REF] Ko | Debugging reinvented: Asking and answering why and why not questions about program behavior[END_REF]. Query-based debugging facilitates the creation of queries over program execution and state using high-level languages [START_REF] Lencevicius | Query-based debugging of object-oriented programs[END_REF][START_REF] Potanin | Snapshot query-based debugging[END_REF][START_REF] Martin | Finding application errors and security flaws using PQL: a program query language[END_REF][START_REF] Ducasse | Object-oriented legacy system trace-based logic testing[END_REF]. Duel [START_REF] Golan | Duel -a very high-level debugging language[END_REF] is a high-level language on top of GDB for writing state exploration queries. These approaches are complementary to our approach as they can be used to create other types of debugging operations.

Omniscient debugging provides a way to navigate backwards in time within a program's execution trace [START_REF] Pothier | Scalable omniscient debugging[END_REF]. Bousse et al. introduce an omniscient debugger targeting an executable Domain-Specific Modeling Language (xDSML) [START_REF] Bousse | Supporting Efficient and Advanced Omniscient Debugging for xDSMLs[END_REF], that while still partially generic can generate domain-specific traces tuned to the actual xDSML. This debugger is partially generic as it treats all xDSMLs in a uniform way. Like our approach this debugger is based on an object-oriented model and aims to improve the debugging process by presenting the user with domain-specific information. The Moldable Debugger, however, is not omniscient but allows developers to fine tune the debugger to the particular aspects of a domain-specific language or application.

Lee et al. propose a debugger model for composing portable mixed-environment debuggers [START_REF] Lee | Debug all your code: Portable mixed-environment debugging[END_REF]. Their current implementation, Blink, is a full-featured debugger for both Java and C. While the Moldable Debugger model does not depend on a particular object-oriented language, we do not provide an approach for crosslanguage debugging (e.g., between an object-oriented and a non object-oriented language).

User interfaces for debugging

Another category of approaches looks at how to improve the user interface of debuggers instead of their actions. Debugger Canvas [START_REF] Deline | Debugger canvas: industrial experience with the code bubbles paradigm[END_REF] proposes a novel type of user interface for debuggers based on the Code Bubbles [START_REF] Bragdon | Code bubbles: a working set-based interface for code understanding and maintenance[END_REF] paradigm. Rather than starting from a user interface having a predefined structure, developers start from an empty one on which different bubbles are added, as they step through the execution of the program. Our approach requires developers to create custom user interfaces (views) beforehand. The Data Display Debugger (DDD) [START_REF] Zeller | DDD -a free graphical front-end for Unix debuggers[END_REF] is a graphical user interface for GDB providing a graphical display for representing complex data structures as graphs that can be explored incrementally and interactively. jGRASP supports the visualization of various data structure by means of dynamic viewers and a structure identifier that automatically select suitable views for data structures [START_REF] Cross | Robust generation of dynamic data structure visualizations with multiple interaction approaches[END_REF]. x DIVA is a 3-D debugging visualization system where complex visualization metaphors are assembled from individual ones, each of which is independently replaceable [START_REF] Cheng | xDIVA: a debugging visualization system with composable visualization metaphors[END_REF].

Each of these approaches introduces different improvements in the user interface of a debugger. To take advantage of this our approach does not hardcode the user interface of the debugger: each domain-specific debugger can have a dedicated user interface. Given that domain-specific debuggers are switchable at run time, when multiple debuggers are applicable a developer can select the one whose user interface she finds appropriate. By focusing only on the user interface these approaches do not provide support for adding custom debugging operations. Our approach addresses both aspects.

Unifying approaches

deet [START_REF] Hanson | A simple and extensible graphical debugger[END_REF] is a debugger for ANSI C written in tksh10 that, like our approach, promotes simple debuggers having few lines of code, and allows developers to extend the user interface and add new commands by writing code in a high-level language (i.e., tksh). Commands are directly embedded in the user interface.

Our approach decouples debugging actions from user-interface components (i.e., widgets): each widget dynamically loads at run time debugging actions that have a predefined annotation. If in deet run-time events are detected by attaching tksh code to conditional breakpoints, we provide a finer model based on combining debugging predicates. Last but not least, we propose modelling the customization of a debugger (i.e., debugging actions + user interface) through explicit domain-specific extensions and provide support for automatically detecting appropriate extensions at run time. In deet developers have to manually select appropriate debuggers.

TIDE is a debugging framework focusing on the instantiation of debuggers for formal languages (ASF+ SDF, in particular) [START_REF] Van Den Brand | TIDE: A generic debugging framework -tool demonstration[END_REF]; developers can implement high-level debugging actions like breakpoints and watchpoints, extend the user interface by modifying the Java implementation of TIDE, and use debugging rules to state which debugging actions are available at which logical breakpoints. LISA is a grammar-based compiler generator that can automatically generate debuggers, inspectors and visualizers for DSLs that have a formal language specification [START_REF] Henriques | Automatic generation of language-based tools using the LISA system, Software[END_REF]. Debuggers are obtained by constructing, from the grammar, transformations mapping from the DSL code to the generated GPL code. Wu et al. present a grammar-driven technique for automatically generating debuggers for DSLs implemented using source-to-source translation (a line of code from a DSL is translated into multiple consecutive lines of GPL code); this makes it possible to reuse an existing GPL debugger [START_REF] Wu | Grammar-driven generation of domain-specific language debuggers[END_REF]. Other language workbenches [START_REF] Faith | KHEPERA: a system for rapid implementation of domain specific languages[END_REF][START_REF] Lindeman | Declaratively defining domain-specific language debuggers[END_REF][START_REF] Pavletic | Extensible debugger framework for extensible languages[END_REF] for developing DSLs or language extensions follow similar ideas: they enable the creator of a DSL or language extension to provide extra specifications during the development of the DSL or language extension that are then used to generate a specialized debugger. Our approach targets object-oriented applications where a formal specification is missing and not programs written in domain-specific languages that have a grammar or another formal specification. Furthermore, if domain concepts are built on top of a DSL, then DSL debuggers suffer from the same limitations as generic debuggers. Our approach directly supports debuggers aware of application domains.

Debugging in domain-specific modelling

Domain-specific modelling (DSM) enables domain-experts to directly work with familiar concepts instead of manually mapping concepts from the problem domain to the solution domain. Debuggers that work at a lower level of abstraction than that of the model limit the ability of a domain-expert to properly debug a model. To address this, the goal of domain-specific modelling is to automatically generate debuggers from a meta-model. While most meta-modelling tools do not automatically generate debuggers, several approaches approach this goal. Mannadiar and Vangheluwe propose a conceptual mapping between debugging concepts from the programming languages (e.g., breakpoints, assertions) and concepts from domain-specific modelling languages that use rule-based approaches for model transformations [START_REF] Mannadiar | Debugging in domain-specific modelling[END_REF]. Kosar et al. discuss debugging facilities for a modelling environment for measurement systems [START_REF] Kosar | Debugging measurement systems using a domain-specific modeling language[END_REF]. Kolomvatsos et al. present a debugger architecture for a domain-specific language used to model autonomous mobile nodes [START_REF] Kolomvatsos | Debugging applications created by a domain specific language: The IPAC case[END_REF]. These approaches take advantage of, and integrate with meta-modeling tools. The approach proposed in this paper is for object-oriented applications where the model consists of objects and relations between them. This model is created by application developers using idioms provided directly by object-oriented programming without the use of a meta-modelling tool.

Conclusions

Developers encounter domain-specific questions. Traditional debuggers relying on generic mechanisms, while universally applicable, are less suitable to handle domain-specific questions. The Moldable Debugger addresses this contradiction by allowing developers to create with little effort domain-specific debuggers that enable custom debugging actions through custom user interfaces. As a validation, we implemented the Moldable Debugger model and created six different debuggers in fewer than 600 lines of code each. Through these examples we show that the Moldable Debugger can reduce the abstraction gap between the debugging needs and debugging support leading to a more efficient and less error-prone debugging effort.

We further explored three approaches for implementing debugging actions. Performance can be impacted, but only when in the special debugging mode. For the regular debugging mode there is no penalty. Thus, our solution offers an opt-in possibility with no cost.

Given the large costs associated with debugging activities, improving the workflow and reducing the cognitive load of debugging can have a significant practical impact. With our approach developers can create their own debuggers to address recurring problems. These custom debuggers come with a price as they have to be constructed by application or framework developers rather than by tool providers. Nevertheless, this can make considerable economical sense when working on a long-lived system. Furthermore, library developers can ship library-specific debuggers together with their product. This can have a practical impact due to the reuse of the library in many applications.

Figure 1 :

 1 Figure 1: The structure of a domain-specific extension.

1

 1

Figure 2 :

 2 Figure 2: A domain-specific debugger for SUnit: (1) diff between the textual representation of the expected and obtained value.

Figure 3 :

 3 Figure 3: A domain-specific debugger for announcements: (1)(3) the receiver and the sender of an announcement; (2) subscriptions triggered by the current announcement.

Figure 4 :

 4 Figure 4: A domain-specific debugger for PetitParser. The debugging view displays relevant information for debugging parsers ((4) Input, (5) Production structure). Each widget loads relevant debugging operations (1.1, 1.2, 2.1, 4.1).

Figure 5 :

 5 Figure 5: A domain-specific debugger for Glamour showing the model of the browser currently constructed.

Figure 6 :

 6 Figure 6: A domain-specific debugger for stepping through the execution of a program at the bytecode level.

2 counter := counter + 1 3

 21 targetObject := Counter new. 4 10000000 timesRepeat: [targetObject increment]

20 Counter>>#initializeWithOne 21 counter := 1 22 Counter>>#returnOne 23 ↑ 1

 211231

  targetObject initializeWithOne] 27 targetObjects := OrderedCollection new: 10000000. 28 10000000 timesRepeat: [ 29 targetObjects addLast: Counter new]. 30 targetObjects do: [:aCounter| aCounter initializeWithOne]

Table 2 :

 2 Performance measurements done on simple examples for step-by-step execution.

	Benchmark	Normal execution Step-by-step execution Overhead
	factorial	1094 ms	2716 ms	2.6×
	merge sort	4 ms	4530 ms	1120×
	parser initialization	192 ms	5881 ms	37×
	parser execution	18 ms	10334 ms	613×
	announcement delivery	19 ms	16419 ms	864×

Table 3 :

 3 Performance measurements of real-world examples for step-by-step execution.

	Listing 1: Factorial	Listing 2: Merge sort
	25000 factorial
		Listing 5: Announcement delivery
	parser := PPJavaParser new.
	parserContext := PPContext new.
	parserContext stream: self getStackJava.
	parser parseWithContext: parserContext

6 collection := 2000 factorial asString. 7 collection sorted Listing 3: Parser initialization: initialize a PetitParser parser for Java code 8 PPJavaParser new 565 Listing 4: Parser execution: parse the source code of the interface Stack 8 from Java using a parser for java code 13 announcer := Announcer new. 14 10000 timesRepeat: [ 15 announcer 16 when: Announcement 17 send: #execute: 18 to: AnnouncementTarget new ]. 19 announcer announce: Announcement

Table 4 :

 4 Performance measurements of simple examples for code-centric instrumentation.

	Instrumented method Predicate	Normal execution	Instrumented execution	Overhead one check	Overhead three checks
	#initializeWithOne	attribute write	81 ms	1317 ms	16×	17×
	#returnOne	method call	83 ms	7664 ms	95×	98×
	#increment	attribute write	103 ms	1350 ms	13×	14×
	#increment	method call	103 ms	7560 ms	75×	77×
	#initializeWithOne	attribute mutation 81 ms	645 ms	8×	-
	#increment	attribute mutation 103 ms	652 ms	6×	-

Table 7 :

 7 Size of extensions in lines of code (LOC).

		Session Operations View Total
	Base model	800	700	-	1500
	Default Debugger	-	100	400	500
	Announcements	200	50	200	450
	Petit Parser	100	300	200	600
	Glamour	150	100	50	300
	SUnit	100	-	50	150
	Spy	-	30	30	60
	Bytecode	20	50	130	200

This section briefly describes this debugger. More details are given in Section 4.2.

We use the notation ClassName>>methodName to identify methods. For readers unfamiliar with Smalltalk code deliver:to:startingAt: is a method that takes three arguments.

More details including demos and installation instructions can be found at: http://scg.unibe.ch/research/moldabledebugger

A version of the prototype, including the code of the benchmarks can be found at http://scg.unibe.ch/download/moldabledebugger/prototype.zip

http://files.pharo.org/vm/pharo

In all code snippets showing code on which we performed a measurement, only the underlined line is the one that is actually being measured; the other lines represent just the setup and are not taken into account in the measurement.

http://hg.openjdk.java.net/jdk6/jdk6/jdk/file/c228a234a3f3/src/share/classes/java/util/Stack.java

http://moosetechnology.org

An extension of Korn shell including the graphical support for Tcl/Tk.

Acknowledgments

We gratefully acknowledge the financial support of the Swiss National Science Foundation for the project "Agile Software Assessment" (SNSF project Nr. 200020-144126/1, Jan 1, 2013 -Dec. 30, 2015). We thank Alexandre Bergel, Jorge Ressia, Mircea Lungu, and the anonymous reviewers for their suggestions in improving this paper.

Object-centric instrumentation

Using this approach there is no longer any runtime overhead in detecting when an attribute read/write, message send or method call happens on a given object. Runtime overhead is introduced by adding conditions that need to be checked when the target event is detected. For example, checking if a method is called on a target object that satisfies an extra condition only incurs runtime overhead for checking the extra condition every time the method is called on the target object.

Basic benchmarks. Given that we use code-centric instrumentations to insert those checks into the unique anonymous class of the target object, the performance overhead will always be lower than or equal to the overhead of just code-centric instrumentations. Consider the two situations below: