All information and materials in this publication are provided by Stellabs and contributors "as is" and any express or implied warranties (including, but not limited to, the implied warranties of merchantability and fitness for a particular purpose) are disclaimed. In no event shall Stellabs or contributors be liable for any direct, indirect, incidental, special, exemplary, or consequential damages (including, but not limited to, procurement of substitute goods or services; loss of use, data, or profits; or business interruption) however caused and on any theory of liability, whether in contract, strict liability, or tort (including negligence or otherwise) arising in any way out of the use of this publication's information or materials, even if advised of the possibility of such damage.
BlueStraggler Scart
SCART is a lightweight tracing tool for Scala (2.10 & 2.11) that generates textual debugging information.
Keeping the impact on readability to a minimum, Scart allows developers to insert traces in the source code without bloating the deliverables.
The syntax is being defined, and the documentation is a work in progress. But an early implementation had been made to identify challenging bugs in a real-life application. It successfully helped pinpoint the issues, hence fix the code, and gave us the motivation to kick off a cleaner implementation as an open-source project.
The source code is released under the Apache License, Version 2.0
Scart now operates with either Scala 2.10.x or 2.11.0
(required version to be specified in build.sbt
)
- SBT 0.13.0 (it will download the Scala compiler automatically if necessary)
- Mercurial or Git (only in order to clone the repository)
- JDK (if bytecode verification is required) or JRE
- a command line shell (e.g. bash)
- v0.02.000
- Instructions to build/test/run Scart without SBT are no longer provided
- Replica on GitHub: https://github.com/stellabs/scart
- v0.02.001
- Improvement of this outline documentation
- Support of Scala 2.11.0
In the case of a public repository, follow the hosting service's instructions.
This describes how to build and test from the command-line using SBT.
To compile, create and publish a Jar on your system, do as follows in
the sbt/scart
directory:
- edit
build.sbt
to update the Scala version you want to compile against - execute
sbt publishLocal
The current tests require human verification, as opposed to automated tests.
In the sbt/test/*
directories:
- edit
build.sbt
to update the Scala version, using the same as the core - execute
sbt run
NOTES
- see the respective
build.sbt
for more information- due to macro stubs, the core JAR must exist before tests can be run on it
An LT's is a basic tracer, the syntax is either as follows:
|++: $$
string|++: %
PRIORITY$$
string
where:
- string can be either:
- a literal string that could identify e.g. an event or position
- an interpolated string that refers to items that have been already evaluated, its usage besides that is free
- PRIORITY is a literal
Int
of value 1 or more if a tracer has a priority greater than the Settings', it becomes disabled - Literal means something which is recognized as such by the compiler i.e. most identifiers aren't literals, but a macro could yield one
|++:
is not an operator and should be used as a statement. It displays the
string in a trace with useful information, when it gets evaluated, and returns
the Unit
value ()
(unless an exception occurs in the interpolated string).
In an attempt to improve the legibility of the original source code, a possible style is to align the LT with the ET, on the same column on the right side.
They represent the most useful and unique feature of Scart. An ET can evaluate seamlessly to the result of the expressions with which it combines. Also, since an ET is a least-priority right-associative operator, most times it may just be appended to the right-end of an existing line of source code. Moreover, by adding extra spaces before the tracer, it's possible to visually separate the "real" code from the parts which are dedicated to traces and have no added value algorithmically speaking.
Expression Tracers are therefore very convenient to use. The syntax is either:
- expression
e_++:
'identifier - expression
e_++:
string - expression
e_++:
PRIORITY->
string
where, in addition to the explanations of the previous section:
- expression is any Scala expression
(even
void
/Unit
method calls evaluate to()
thus are expressions) - 'identifier is a Scala literal
Symbol
used to identify the trace or the expression (e.g. vals, vars, objects)
e_++:
shows in the traces, by default, when the expression gets evaluated
(and not before, e.g. lazy val
):
- the identifier or string
- the result of the expression, as a
String
(usingObject#toString
) - also the exception, if it catches one (that it re-throws)
However, catching and re-throwing exceptions isn't always best because,
as a side effect, the exception stack will memoize a position in the tracer.
That can be avoided by replacing e_++:
with x_++:
.
The latter:
- doesn't catch exceptions but can detect one; that shows in the trace
- however, it is unable to identify the exception: the trace gives no ID
Replacing the ++
part with --
within an LT or an ET disables it at the
source code level. Although that ought to be done as a temporary solution
only, for instance to remove a noisy trace, that is convenient sometimes:
- the whole project containing that source file need not be cleaned just to disable the trace
- it's more legible and searchable (e.g. with grep) than commenting the trace when the developer wants to inhibit the tracer rather than deleting it
The Scart JAR doesn't include default settings, on purpose, to avoid mistakes such as the delivery to a customer of an application that contains traces. Instead, the control is left to the developers and/or people responsible of the build. Therefore, the Settings have to be provided or Scart won't work and build errors will appear.
For instance, a source code file that designates the settings could have the following content:
package org.stellabs.scart.config
import org.stellabs.scart.tracing.{SettingsBase, SettingsByDefault}
final object Settings extends SettingsByDefault
{
import language.postfixOps
import SettingsBase._
protected val trace = `ON`
val priority = 5
val formatter = Formatter `all-last`
val printer = Printer `System.err.println`
}
where:
trace
is the main switch for enabling (ON
) or disabling (OFF
) tracingpriority
is aInt
of value 0 (highest) or more. If a tracer has a priority, its trace will be output only if its value is equal or less than the value of this settings priority. Tracers without priorities always emit unless, of course, the switch isOFF
.formatter
designates under which format each trace will be emitted:
as-is
: only the entry string/identifierall-first
: the entry is followed by its source file name, line#, column#, class or method nameall-last
: the entry is preceded with its source file name, line#, column#, class or method nameinclosure-only
: the entry is not emitted, only the class or method name is (useful for example to identify endless loops without all the clutter)
printer
: is theString => Unit
function that actually outputs the traces. Developers may provide their own function, but there are predefined printers:System.out.println
uses its synonymic methodSystem.err.println
uses its synonymic method
NOTES
the highest priority applicable in tracers must be 1 (one), as opposed to the Settings object's 0 (zero)
For demonstration purposes, in this document, traces are printed via
System.err.println
. However, the Setting'sprinter
could use a Logger, for instance by the definition of something like:val printer = {s:String => getLogger(...).trace(..., s)}provided that the
getLogger
method (or function) and its arguments are defined before the project's compilation (as it is required in the case of macros).
The Settings
object must be compiled ahead-of-time:
- the Scart JAR contains macros; the compiler invokes them when they are used in a project's source code
- the settings designate how the Scart macros behave if invoked; in the example above, traces of priority 6 or more won't be generated at all (nothing at the bytecode level) even if they exist in the source code
Given the following source code, as a tiny example:
package com.example
import org.stellabs.scart.tracing._
object KillerApp extends App{ ;|++: $$ "Goodbye, Underworld!"
val str = args.mkString(" ") e_++: 'str
println(s"Hello, World! ($str)") e_++: 5-> "println(...)"
}
With the Settings set as illustrated earlier, sbt "run a b c"
will output
something like:
> /sandbox/killerapp/src/KillerApp.scala(L5,C44)[class com.example.KillerApp]: Goodbye, Underworld!
> /sandbox/killerapp/src/KillerApp.scala(L6,C44)[class com.example.KillerApp]: str="a b c"
Hello, World! (a b c)
> /sandbox/killerapp/src/KillerApp.scala(L7,C45)[class com.example.KillerApp]: println(...)=()
where the traces, preceded with >
, show respectively:
- the source file name, line#, column#, class name and the LT's literal string
- the contents of
str
, the nextprintln
shows the ET didn't alter it - the
println
's return value:()
as it'sUnit
The last Expression Tracer's priority being 5 (5-> ...
). If the Setting's
priority is changed to 4 (val priority = 4
) and the project cleaned, that
trace won't show as illustrated by the new output:
> /sandbox/killerapp/src/KillerApp.scala(L5,C44)[class com.example.KillerApp]: Goodbye, Underworld!
> /sandbox/killerapp/src/KillerApp.scala(L6,C44)[class com.example.KillerApp]: str="a b c"
Hello, World! (a b c)
Finally, all tracers can be inhibited by switching them off in the Settings,
protected val trace = `OFF`
; that gives the output:
Hello, World! (a b c)
There are many more features than illustrated above, but a proper documentation remains to be done. For the time being, it's possible to proceed with the options that follow.
-
Using the tests as models (recommended)
Although they have exhaustive contents (therefore not good examples of readability), the current tests basically are applications that use Scart to emit traces. Thus it's possible to read them to understand how to combine the Scart JAR and its settings with a given application, in order to emit debug traces:
- designate settings based on
test/**/scart/TraceSettings.scala
- set traces as demonstrated in other source files
test/**/*.scala
- create or modify a
build.sbt
file; it can be modeled onsbt/test/**/build.sbt
- in the directory that contains the application's
build.sbt
, runsbt
with the applicable arguments
-
Using the extra template (quick and easy)
A basic project template is available in
sbt/template/inline
. Developers without prior exposure to SBT from the command-line might prefer to start with this solution.
- copy the entire contents of the
inline
folder at your favorite place - for a new project, use the source template
killerapp/src/KillerApp.scala
- add traces in your source files
- update
build.sbt
with the data of your project (e.g. paths and names) - also designate your settings in
build.sbt
, the source file will be generated - from the command line, do:
sbt run
orsbt "run <arguments>"
, that builds and executes the application
For more, follow the instructions written in the respective build.sbt
.
IMPORTANT CONSIDERATIONS
Tracers, the mechanisms that produce traces, aren't phantom constructs.
The 2 kinds of tracers do evaluate to something.
- Line Tracers should be used like statements and are
Unit
.- Expressions Tracers don't change the value or the type of an expression, as explained earlier.
When a tracer is disabled or inhibited, its type won't change (type-safety is preserved), only its behavior will change, to not output any tracing information, aka trace, that it would emit otherwise.
A tracer's insertion in the source code is thus quite seamless. However, Line Tracers should never be inserted in places such as:
- between an
if
and its non-enclosed result- between a
for
/while
and its non-enclosed body- as the last statement of non-
Unit
expressions (e.g. in case of an implicitreturn
)=> if inserting
{}
somewhere would alter the behavior, inserting a Line Tracer there would alter the behavior as well.Avoid creating side-effects in traces, for example:
- when tracing with interpolated strings e.g.
s"My counter is: ${myCountVar+=1; myCountVar}"
- subtler, by using lazy vals or by-name parameters that haven't been used yet
=> that's because the behavior will be altered when traces are enabled (and only when enabled, in both these cases).
As with Scala macros, each project that makes use of Scart should depend on a separate "Settings project" that defines the Scart Settings object:
- the user project must have a dependency on the Settings project
- optionally depend on the Scart JAR, that varies with the configuration below
- the Settings project must either:
- depend on the Scart JAR
- contain the Scart Core source(s) (but exclude the Scart stub(s), tests etc.)
NOTES
- if Scart was built using
sbt publishLocal
, its JAR should exist somewhere under the developer's home directory.- the Scart core source code is under
compile/main/src/
in this repository.- when the Settings project change, it should be rebuilt, then the user project should be cleaned and rebuilt. Whether that happens automatically or not might vary with the IDE.
- Scart Core, Scart Settings and User Project should be all compiled using the same version of the Scala compiler
Finally, there's NO NEED to remove Scart traces from the source code at the end in order to deliver or deploy:
- just change, in the trace settings
val trace = `ON`
to
val trace = `OFF`
- then recompile the application entirely => your project will get rid of all traces.
In order to make sure that the new settings take effect, the dependent
project(s) should be cleaned. For example, the test cases released together
with the Scart core can be re-run with:
sbt clean update run
With the OFF
switch, not only the traces won't show at run time, better, they
won't exist anymore at the Java bytecode level.
No binary bloat!
NOTES
Conversely, since enabling traces (switch
ON
) implicitly adds calls, strings etc. the content can become quite heavyweight. Tracers inserted in the source code are similar to syntactic sugar, but they can abstract powerful features.Should that be necessary, the presence (or absence) of traces in the bytecode can be verified with e.g. the command:
javap -c -private -classpath <dirs-and-jars> <class-qualified-basename>
By so doing, the content of dumped JVM instructions from classes is expected to be far more lightweight when the traces are disabled (as though there were no traces in the source code at all, with the exceptions already mentioned).
If that's not the case, there is probably a build issue; the project must be cleaned in order to remove the cached classes and rebuild it from scratch.
In comparison, most logging tools leave the same bytecode regardless of the actual settings. That is normal since they serve a different purpose: providing information even after deployment. Scart, however, is meant to help developers fixing bugs in the code; depending on the settings, the amount of low-level debug information can be huge thus not proper to emit during operations.
Copyright 2013, 2014 Stellabs.