When I presented my recent work on Varan recently, more than one person asked me
a variant of the following question:
I’ve been using ptrace
to force the exact same execution on two processes by
ensuring both read the same data from their system calls. Still, at some point,
they start behaving differently (they diverge). Why?
ptrace
is a feature of the Linux kernel. Quoting it’s manual page:
The ptrace() system call provides a means by which one process (the “tracer”)
may observe and control the execution of another process (the “tracee”), and
examine and change the tracee’s memory and registers. It is primarily used to
implement breakpoint debugging and system call tracing.
When the tracee issues a system call, the tracer gets notified with the number
of the system call and its arguments. That alone is quite useful. For
instance, strace
uses ptrace to dump the sequence of system calls that a
process issues.
Besides observing the tracee, ptrace
also allows the tracer to attach the
tracees memory. As a result, the tracer can emulate the tracee’s system calls
without actually executing them.
For instance, let us consider that we have two processes, P1 and P2, running
the same program. We can use ptrace
attach both processes and copy the
results of all the system calls from P1 to P2. Given that the kernel
mediates all inputs that a program ever receives, this way we ensure that the
two processes get the same inputs.
Let us consider now a simple example. The following function generates a random
number by XOR-ing the current time, and the process PID.
#define MORE_RANDOMNESS(X) ((X) = 0)
unsigned long random() {
unsigned long ret;
MORE_RANDOMNESS(ret);
struct timeval t;
gettimeofday(&t);
ret ^= t.tv_sec ^ t.tv_usec;
int pid = getpid();
ret ^= (unsigned long)pid;
return ret;
}
This function issues two system calls:
gettimeofday
and
getpid
. Using ptrace
as described above, we can ensure that both processes obtain the same time and
the same PID. Therefore, variables t
, pid
, and ret
should match between
processes P1 and P2. Right?
Wrong
In this post, I explain the main reasons why, according to my experience.
Virtual System Calls
If you run the program that I show above under strace
on an x86 machine, you
will notice the absence of system call gettimeofday
from the trace. Why?
System call gettimeofday
in x86 is a virtual system
call. The Linux kernel
uses virtual system calls to accelerate common system calls that may execute
outside of the kernel. The idea is that such “system calls” execute as a simple
function call, rather than an expensive context switch to kernel mode. The
Linux kernel exposes a virtual dynamic shared object (vDSO) as an ELF file
mapped in every process and discoverable through the auxiliary
vector. Then, at runtime, the C standard
library (libc) scans the auxiliary vector, finds the vDSO, and calls those
functions instead of issuing system calls. Of course, as programs do not issue
system calls directly and use libc instead, this is all completely transparent
to the program using such virtual system calls.
The speed difference between virtual system calls and regular system calls is
quite noticeable. This
program
issues 1.000.000 gettimeofday
system calls, first through libc (and thus using
virtual system calls) and then manually through hand-written assembly. Here’s
the output on my machine:
> gcc -O0 example.c
> time ./a.out # system call
./a.out 0.04s user 0.08s system 97% cpu 0.123 total
> time ./a.out 0 #virtual system call
./a.out a 0.03s user 0.00s system 91% cpu 0.033 total
That’s a 3x difference!
Getting back to why ptrace
fails, in this case there is no system call for
ptrace
to intercept. As a result, P1 and P2 get different values for t
,
thus generating different values for ret
.
glibc get_pid
glibc is the GNU implementation of the C
standard library (libc), and the de-facto libc used in GNU/Linux. Until
recently, glibc optimized system call getpid
by caching the PID when
initializing (before the program’s main
function is called). Then, function
getpid
simply reads the cached PID without issuing any system call.
The program I show above uses getpid
to generate the random number. In our
scenario, if we wait until P1 and P2 reach their respective main
function
to start matching their system calls through ptrace
, we miss the caching of
the PID. Therefore, each process gets a different value for variable pid
.
There’s yet another surprise in how glibc caches the PID. Instead if issuing
the expectable system call getpid
, glibc uses the rather unlikely system call
set_tid_address
for that. So, when using strace
to detect the point in the program execution
where the PID is read and cached, look out for system call set_tid_address
.
This approach of caching the PID has gathered some criticism. Linus Torvalds
himself uses colorful language to describe how he feels about this
optimization. Recently,
glibc removed this
optimization,
which is not present since, and including, version 2.25 (released on
5/Feb/2017).
Non-determinism
The rationale behind synchronizing two processes at the level of system calls
assumes that the programs that the processes execute are deterministic. This is
a reasonable assumption, given how programs typically gather any non-determinism
they use vie the operating system through system calls (e.g., by reading file
/dev/random
, or by getting the date through system call gettimeofday
). As a
result, the programs themselves are deterministic modulus the inputs from the
operating system.
However, and unfortunately, programs can exbibit non-determinism without
interacting with the operating system. This is a basic limitation not only of
ptrace
, but of any other method that matches system calls between processes.
Let’s look at a few examples of such non-determinism that I found in practice.
rdstc
Instruction
The x86 instruction rdtsc
reads the value of the timestamp counter, which is a
64-bit register that increases monotonically at every clock cycle since the
processor was reset. It was introduced with the Pentium processor and it was a
great way to get accurate performance timing for programs. However, it is not
usable for that purpose nowadays anymore, due to out-of-order execution, several
logical cores on the same physical CPU, context switching between threads and
processes, etc.
Still, some existing code uses this instruction to generate low-quality random
data very and monotonically increasing timestamps very fast. For instance, going
back to our example, suppose that macro MORE_RANDOMNESS
is defined as:
#define MORE_RANDOMNESS(X) ((X = rdtsc()))
In this case, the value of variable ret
will never be the same between
processes P1 and P2.
The code example we’ve been following, and this section on instruction rdtsc
,
were inspired by function
mktemp
in glibc.
Function mktemp
takes a template filename ending in XXXXX and returns an
unique filename (e.g., mktemp(/tmp/fileXXXXXX) = /tmp/file123456
). The
example is basically how mktemp
is implemented for
x86.
Undefined Behavior
Let us now consider that macro MORE_RANDOMNESS
is defined as follows:
#define MORE_RANDOMNESS(X) ((X))
This results in a C program with undefined behavior, as variable ret
is read
without being initialized. Of course, a program written in C that displays
undefined behavior has no meaning and any behavior (of the whole program) is
acceptable.
So, a program that displays any undefined behavior is incorrect and we should
limit our discussion to correct C programs, right? Well. Some programs have
intentional undefined behavior. For instance, OpenSSL initializes a random
data generator with an uninitialized byte
array,
which provides a small amount of extra entropy as the runtime value of the
uninitialized array is hard to predict. This is by design and can be disabled
by setting flag -DPURIFY
when building
OpenSSL.
Using a -DPURIFY
build in production did lead to a vulnerability in
Debian worthy of being
mentioned in an xkcd comic. Still, the most
recent version of OpenSSL (1.1.0) uses -DPURIFY
by
default.
Getting back to our example, as with rdtsc
, this means that variable ret
has
a different value between P1 and P2, depending on the “garbage” contents of
the uninitialized stack buffer. However, note that running the same program
compiled by the same compiler with the same optimization level on P1 and P2
may lead to the same “garbage” being left on the stack, which hides this
divergence. Still, changing any of those variables (using different compilers
or different optimization levels) makes processes P1 and P2 diverge.
Multi-threading
Processes that use several threads have access to another form of
non-determinism in the shape of thread scheduling. For instance, we can imagine
a program in which two threads race to grab a lock, and then each thread opens a
file. Which thread wins the race depends on the scheduler, and may be
different between P1 and P2. This issue can be solved with deterministic
scheduling or by imposing a
total order between system calls, as Varan
does.
Rubah is a Dynamic Software Updating system for Java that works on the stock
Oracle HotSpot JVM, does not add any measurable overhead when running a program,
and performs dynamic software updates efficiently.
In this post, I explain how Rubah uses the low-level unsafe operations available
in class sun.misc.Unsafe
(which I shall refer to as the unsafe API) to
implement the optimizations that make it so efficient. I start by explaining
what the unsafe API is and how to use it, then I describe the object memory
layout of the Oracle HotSpot JVM and how to explore it with the unsafe API, then
I discuss how Rubah does that to improve its performance, and finally I propose
how to make some of the unsafe operations safer in a way that is compatible with
Rubah.
What is sun.misc.Unsafe?
The class sun.misc.Unsafe
is a proprietary API that enables a Java program to
escape the control of the JVM and perform potentially unsafe operations, like
direct memory manipulation. Here is a list of interesting methods that this API
has (more documentation available
here):
public class Unsafe {
// use reflection instead, this method throws an exception if the calling
// class is not on the bootstrap classpath
public static Unsafe getUnsafe();
// static field/Object field/array manipulation utilities
public Object staticFieldBase (Field f);
public long staticFieldOffset (Field f);
public long objectFieldOffset (Field f);
public int arrayBaseOffset (Class c);
public int arrayIndexScale (Class c);
// reference-type field manipulation
public Object getObject (Object o, long offset);
public void putObject (Object o, long offset, Object x);
// int-type field manipulation (repeated for every other primitive type)
public int getInt (Object o, long offset);
public void putInt (Object o, long offset, int x);
// compare-and-swap object/int/long
public boolean compareAndSwapObject (Object o, long offset, Object expected, Object x);
public boolean compareAndSwapInt (Object o, long offset, int expected, int x);
public boolean compareAndSwapLong (Object o, long offset, long expected, long x);
// allocate instance without running constructors
public Object allocateInstance (Class cls);
// try entering a monitor, fail with false instead of blocking if not able to
public boolean tryMonitorEnter (Object o);
}
For instance, the following code sets an entire integer array to 1 using the
unsafe API to avoid any bounds check:
// For this to work, add the class to the bootstrap classloader by passing
// the option -Xbootclasspath/a:. to the java command
Unsafe u = Unsafe.getUnsafe();
int size = 1 << 16;
int[] array = new int[size];
int base = u.arrayBaseOffset(Integer.class);
int scale = u.arrayIndexScale(Integer.class);
for (int i = 0 ; i < size ; i++)
u.putInt(array, (base + i * scale), 1);
// Nothing prevents me writting out of bounds:
// u.putInt(array, (base + size * scale), 1);
// Or reading:
// u.getInt(array, (base + size * scale));
While this is faster than regular Java array manipulation, it may lead to
out-of-bounds accesses that can be exploited
maliciously.
The unsafe API is used extensively throughout the java.util.concurrent
package
to perform atomic compare-and-swap operations and volatile read/write on arrays.
Currently, that is the only way to do those operations. However, the Oracle JVM
development team is looking into ways to make this API safe and part of the Java
API.
Most of the low-level memory operations can also be made through JNI native
code. However, using the unsafe API is more efficient because it avoids the cost
of switching context from Java to JNI and back. Besides, the JIT compiles some
calls to the unsafe API directly to JVM intrinsics.
HotSpot JVM object memory layout
There is a code pattern when using the unsafe API to manipulate data in object
fields or arrays. The code above shows an example of that pattern: First, it
gets a base pointer with method arrayBaseOffset
, then, it gets the array scale
factor with method arrayIndexScale
, and ,finally, it computes the formula
base + i * scale
to manipulate position i
on the array with methods getInt
or putInt
.
A similar pattern applies to manipulating objects, the following example shows:
// For this to work, add the class to the bootstrap classloader by passing
// the option -Xbootclasspath/a:. to the java command
Unsafe u = Unsafe.getUnsafe();
LinkedList obj = new LinkedList();
Class c = LinkedList.class;
Field f = c.getDeclaredField("first");
long offset = u.objectFieldOffset(f);
u.getObject(obj, offset);
u.putObject(obj, offset, null);
// Nothing prevents me from writting a wrong type:
// u.putObject(obj, offset, c);
For manipulating object fields, we do not get a base pointer and a scale factor.
We get, instead, the offset of each individual field with method
objectFieldOffset
. We can then manipulate object fields using methods
getObject
and putObject
. Note that these are the same methods as in the
previous example: If field first
had type int
, we would use methods getInt
and putInt
.
Both these code patterns map directly to how the JVM lays objects in memory. The
following figure shows how objects look in memory (memory addresses of
variables/fields from the code above are writen in a C-like style with a
preceding &
):
Given the similarity between the HotSpot and the OpenJDK, I shall use the
OpenJDK names in the description of the object memory layout, with links to the
relevant header files in the OpenJDK source. Every object starts with a fixed
sized header that has two fields, as defined by the header file
src/share/vm/oops/oop.hpp:
- _mark:
Defined in header file
src/share/vm/oops/markOop.hpp.
One word that contains either:
- Unlocked object:
- hash: Identity hash code;
- age: GC information about the age of the object;
- Some unused bits to keep the field word-aligned;
- Locked object:
- ptr: pointer to where the header is (either on the stack or wrapped by an inflated lock);
- lock: state of the lock (biased/inflated), 001 means not locked;
- _klass: Defined in header file
src/share/vm/oops/klassOop.hpp.
Quoting the source: “A klassOop is the C++ equivalent of a Java class”.
This is where the vtable is located, together with more low level information
about each object of each particular class, such as its size and the offset where
to find each field.
Arrays also have a header that starts with the same two fields, followed by an
extra field, as defined by header file
src/share/vm/oops/arrayOop.hpp:
- lenght: Number of elements in this particular array
Manipulating the object model
Rubah uses the unsafe API to manipulate the metadata on the object header. This
is extremely unsafe and getting it right was itself an important
implementation challenge. This is also brittle because the unsafe API is
not part of the standard Java API. Future versions of the HotSpot JVM are free
to change how objects are layed out in memory.
In the following, I list all the different ways Rubah manipulates the header
using the unsafe API:
-
Direct field access
To migrate the program state, Rubah needs to traverse every object it finds.
This means accessing every field, which may not be publicly visible. The unsafe
API gives Rubah unrestricted and efficient access to every field of every
object.
Also, both migration algorithms that Rubah has (parallel and lazy) use
compare-and-swap to ensure correctness while migrating the program state. The
unsafe API is the only way to perform this operation on regular fields.
-
Identity hash-code
Rubah migrates objects between versions while keeping their identity. It does
so by creating a new object in the new version that will replace the outdated
one, and then migrating the state of the old object to the new one. When
traversing the program state, Rubah replaces all references to the old object by
references to the new one. This way, if two references are ==
to each other in
the old version, they will also be ==
after the update takes place.
However, the identity hash-code of such two objects will differ, and this can
break the program semantics. For instance, if the class does not override the
hashCode()
method and the program keeps these objects in a java.util.HashMap
,
which is perfectly valid, then Rubah would break the semantics of the program:
After the update, the objects will be on the wrong buckets of the HashMap and,
therefore, impossible to find.
Initially, Rubah solved this problem by rewritting the bytecode of all classes
to add an extra field to keep the identity hash-code and to change every
constructor to initialize such field. Rubah also added an hashCode()
method
to all classes that did not have any that just returned the value of the extra
field. With this semantics preserving rewritting, migrating the identity
hash-code between versions is just a matter of copying a field. However, this
prevents the JIT compiler to use intrinsics to access the identity hash-code
efficiently and prevented the JVM from initializing hash-codes lazily. As a
result, this added about 5% overhead to steady-state execution. Also, it does
not work for arrays.
Rubah now writes the identity hash-code of the new instance directly in the
object header using the unsafe API. This works objects and arrays and removes
the performance overhead. The following code example, adapted from a class in
Rubah called
UnsafeUtils,
shows how Rubah does that:
// For this to work, add the class to the bootstrap classloader by passing
// the option -Xbootclasspath/a:. to the java command
Unsafe u = Unsafe.getUnsafe();
Object o = new Object();
// Valid for a 64 bit JVM with compressed oops
// Might not work for different architectures
long offset = 1L;
int newHash = 42;
int identityHashCode = System.identityHashCode(o);
int unsafeHashCode = u.getInt(o, offset);
assert (identityHashCode == unsafeHashCode != newHash);
u.putInt(o, offset, newHash);
int identityHashCode = System.identityHashCode(o);
int unsafeHashCode = u.getInt(o, offset);
assert (identityHashCode == unsafeHashCode == newHash);
-
Changing the class of existing objects
Rubah’s’ lazy program state migration algorithm introduces the concept of
proxies, used to intercept method invocations on outdated objects that need
migration. Each proxy class extend the proxied class and override all methods so
that Rubah can intercept method calls. At the object layout level, this means
that proxies have the same fields on the same offset but a different _klass
.
So, all Rubah needs to do is to install the proxy _klass
on existing objects
to turn them into proxies.
Besides proxies, Rubah also changes the class of existing objects for another
reason. Between two versions, the vast majority of objects do not need to be
migrated because their class was not updated. However, consider the following
example: Class A changed, class B did not change and has a field of type
A. Of course, we have to migrate every instance of A. However, class
B has the same layout in memory in both versions. Rubah just needs to
update the code of class B to use the field with the correct new type.
Both this scenarios illustrate the need for Rubah to change the class of
existing objects by using the unsafe API to adjust the _klass
field on the
object header. This is the most brittle optimization that Rubah performs. In fact,
getting it to work was a challenge. In part, because the code the JIT compiler
emits assumes that the _klass
does not change during the execution of a method.
If it does, the JVM crashes. So, Rubah is carefully implemented in a way
that prevents the code that changes the _klass
from ever being inlined with
application code.
Besides the JIT compiler, the garbage collector uses the _klass
to access
the size and structure of objects it visits. However, this is not as problematic
because both the old and the new _klass
fields agree on the same information
the GC needs to do its job. Therefore, the regular GC operation does not cause
the JVM to crash when Rubah modifies the _klass
field.
Note that the alternative to deal with instances of unchanged classes between
versions would be to either: (1) Copy them or (2) erase the type of all fields
to java.lang.Object
and inject the appropriate type cast before every bytecode
that manipulates fields. Option (1) would always require a deep copy of the
whole heap at every update, so we discarded it from the start. Early versions of
Rubah implemented option (2), which added a steady state overhead of
5%.
The following code examples, adapted from class
UnsafeUtils
in Rubah, shows how to change the class of an existing object by manipulating
the _klass
field:
class A { /* empty */ }
class B { /* empty */ }
// For this to work, add the class to the bootstrap classloader by passing
// the option -Xbootclasspath/a:. to the java command
Unsafe u = Unsafe.getUnsafe();
Object a = new A();
Object b = new B();
// Valid for a 64 bit JVM with compressed oops
// Might not work for different architectures
long offset = 8L;
// Do not keep this around for long, it changes over time
int klass = u.getInt(a, offset);
assert (a instanceof A);
assert (b instanceof B);
u.putInt(b, offset, klass);
assert (a instanceof A);
// If this code gets JITed, the following line may terminate the JVM with SIGSEGV
assert (b instanceof A);
How to make sun.misc.Unsafe safer
Useful as it may be, sun.misc.Unsafe
is a proprietary API that will disappear
or be modified in future releases of the HotSpot JVM. Rubah relies on features
of the unsafe API that might disappear. So, in this section, I propose some
alternatives to make those features safe so that they can be made part of the
standard Java API.
-
Identity hash-code
The simplest approach, assuming that a method similar to
sun.misc.Unsafe.allocateInstance
makes it to the standard API, is to add an
integer argument that sets the identity hash-code to be the least significant
bits (the size of a Java hash-code) of that integer argument:
// allocate instance without running constructors
public Object allocateInstance (Class cls);
public Object allocateInstance (Class cls, int hashCode);
Assuming that this method does not make it to the standard API, Rubah could
still allocate instances without running any real constructor by injecting dummy
constructors to every class that do not do anything interesting. However, Rubah
needs to set the identity hash-code of the new object being constructed.
In this scenario, one key observation is that it is safe to set the identity
hash-code of any object being constructed before any reference to that object
escapes the constructor. The invariant that the identity hash-code does not
change during the lifetime of the object is kept, except for the constructor
code that actually changes the identity hash-code. I claim that this behavior is
safe and acceptable.
The bytecode sequence that creates an object involves a NEW
instruction and
a INVOKESPECIAL
instruction to invoke a constructor of the superclass on the
newly created object. Between these two bytecodes, the object is instantiated
but not constructed. The JVM performs escape analysis to reject any bytecode
that leaks references to this object by writing it to some field or passing it
as an argument to some method.
This is the right moment to set the identity hash-code of the new object. One
option is to add a special API method. However, this involves passing the
instantiated but not constructed object to that method, which makes the bytecode
verifier to reject such bytecode. This option thus require modifications to the
bytecode verifier:
NEW java.lang.Object
DUP
ICONST 42
INVOKESTATIC java.lang.System.setIdentityHashCode
INVOKESPECIAL java.lang.Object()
Another option is to add an extra constructor to java.lang.Object
that takes
an integer and sets the identity hash-code to that value. The bytecode verifier
remains unchanged, but we are now exposing a new constructor that should almost
never be used. This problem could be mitigated by making this method invisible
to the compiler and only accessible through reflection:
Object.class
.getConstructor(new Class[]{ Integer.class })
.invoke(new Object[]{ new Integer(42) });
Yet another option is to have the developer add a constructor that takes an
integer as the first argument and has a special annotation to note that such
argument is actually the identity hash-code of the object being constructed. Or,
instead of an integer, that argument has a special type (e.g.
java.lang.IdentityHashCode
), so that it does not collide with any existing
constructor that already takes an integer:
public class A {
@IdentityHashCodeConstructor
public A(java.lang.IdentityHashCode hash) {
}
}
-
Changing the class of an object
The JVM keeps the invariant that the class of any given object does not change
during the lifetime of that object. Therefore, no matter how safe we make this
operation, it violates this invariant by design. This is our starting point in
making this operation safe.
Rubah gets away with it in part because it traverses the heap and fixes
references to instances of java.lang.Class
. So, if some structure maps classes
to objects of that class, Rubah changes the class of every object and all
references to the instance of java.lang.Class
associated with the outdated
class (e.g. Rubah supports updating instances kept in instances of
java.util.EnumMap
, which does something similar).
However, finding some way to relax this invariant allows efficient
implementations of proxies. A proxy, in this sense, is a class that extends the
proxied class, does not define any fields, and overrides all methods to redirect
the invocation to some other new method that the developer can customize. From
the point of view of the object memory layout, a proxy looks exactly the same as
the proxied instance (same size, same fields at the same offset) except for the
_klass
field in the object header. Proxying an object, or turning an existing
proxy into real objects, can be as simple as a writing over the _klass
field.
void changeClass(E object, Class<? extends E> newClass)
throws IllegalArgumentException;
To change the class of an object safely to another class that defines a
different set of fields in this way, by changing the _klass
field, we have to
place restrictions on how different the set of fields can be. The idea is that
the representation of the object in memory should, at least, have the same size.
So, we can require the new class to define the same number of fields of the same
broad type (reference or same primitive type).
If this pre-condition is met, we can take a CLOS-like
approach to map
the fields: Pass, as an argument to the method that changes the class, an
object that implements method mapFields
which takes two maps from fields to
their values and initalizes the new map given the values on the old map:
interface Mapper {
void mapFields(
Map<Field, Object> oldMap,
Map<Field, Object> newMap);
}
void changeClass(E object, Class<? extends E> newClass, Mapper mapper)
throws IllegalArgumentException;
Hello!
I use Latex to write all my
technical documents. I use it also to make slides that I
show at my presentations. So, I end up with a PDF that contains the slides.
The problem
To show a PDF slideshow, we require some support from the PDF reader software.
At the very minimum, it needs to support fullscreen, which the vast majority
does. Some go a little further. For instance,Okular shows the current slide number and a
visual representation of how many slides are still left.
But that’s it. If we take a look at other presentation software, such as as
Libre Office’s Impress, we see that it
takes advantage of a second monitor to show a presenter’s console that features:
The current and next slide, the time elapsed and remaining, the notes that we
wrote for each slide, and some other information.
Using two monitors to present your slides is a very common usage scenario (laptop connected to a overhead projector) and the presenter screen is really, really useful. I can’t stress that enough.
The (incomplete) solutions
Let’s take a look at what software is there available specifically for presenting PDF slides, splitted into three classes:
- Slide transition effects:
- Presenter console support:
- pdf-presenter-console: Supports current and next slide, and displays time information. Does not support notes.
- pdf-presenter: Uses two windows, one for the presenter console and other for the slide. Support current and next slide, and loads notes directly from the PDF (I have not tried this option). Does not display time information.
- Unsupported/abandoned:
The first class of programs bring some “eye-candy” to your presentations. I have
used impressive, and it works really well. It pre-loads all the slides, so
changing slides is really fast. It also animates the slide transition. A fast
crossfading between slides looks great, more elaborate transitions just looks
like showing off. I have not tried any of the others, but I guess they all
should do more or less the same.
Pdf-presenter-console and pdf-presenter support a presenter console. However,
none has all the features that I value. One supports notes but does not support
time information, the other supports time information but does not support
notes.
My solution
I could not find any PDF presenter program with all the features that I would
like to have. So I made my own!
I present you the brand new
open-pdf-presenter. Version 0.1
was release yesterday! You can find on the
wiki instructions about
building and running it.
Here are some screenshots:
Currently, it displays a presenter console with the current slide, the next
slide, the elapsed/remaining time, and how many slides are left. In the near
future, it will support notes on a separate file and some other nice features
from existing software:
- Fade slide screen to white/black (useful when projecting on white/black boards)
- Grid with thumbnails of all the slides (useful when someone in the audience wants to show that first slide after the second performance chart)
It is open-source, so feel free to inspect and modify my code. It uses git as the revision control system. If you make an useful and working patch, just use git to format that patch and email it to me.
I welcome all feedback (bug reports, feature requests, patches, trolling, rants, death
threats) so feel free to leave your own. I’ll keep you posted when I
make another release.