[comp.windows.x] Mouse-tracking experiments in X11

msm@SRC.DEC.COM (Mark S. Manasse) (05/09/89)

Several weeks ago, I got into a minor name-calling skirmish with 
some other contributors to xpert about how to really do mouse-tracking 
in X.  Rather than continue to foam at the mouth, as is my usual 
habit, I decided to treat this as an opportunity to indulge briefly 
in the scientific method.  And guess what -- we were all wrong.  
I apologize for the delay in promulgating these findings, but, except 
in electrochemistry, the findings of science come slower than flaming.  
And, in the interests of science, I've included my program below, 
so you can all try to reproduce my results.  Deuterium not included.

Allow me to recap, for those of you who may be wondering what I'm 
getting on about.  One of the puzzling things about network window 
systems has been how to get good performance for mouse-tracking without 
consuming too much in the way of system resources. 

There are three plausible ways to track the cursor in X11, say for 
the purposes of rubber-banding a rectangle.  The first is to poll 
the cursor position rapidly, painting whenever you detect changes 
in the position of the cursor.  This gives you the lowest latency 
of all possibilities in return for the maximum consumption of server, 
client, and network resources.

You'll recall that a month or two ago I asked you to run some 
experiments for me on cursor-tracking in X.  Well, you did, and thank 
you.  I have some results now, and I'd like to let you know what they 
are, although I'm not sure how much they mean.  We'll refer to this 
style of interaction as POLLING.

A second possibility is to ask the server to send you motion events 
whenever the cursor moves.  This decreases the amount of load, if 
event delivery is faster than the sampling rate for the mouse, and 
increases it otherwise.  By doing event compression on the client 
side, and adding a sync call after painting, we can arrange things 
so that even in case of heavy load, the server and client never get 
farther out of sync than is necessary.  We'll refer to this style 
as MOTION.

The final possibility is to use PointerMotionHint.  In this mode, 
the X server delivers a position only after a button or key event 
or window crossing.  Having delivered the position, it doesn't send 
any more until you ask it to send you one the next time the mouse 
moves.  This uses very little bandwidth or resources on the server 
and client, but it does require an extra round-trip for each reported 
position when things are running fast, when compared to MOTION.  We'll 
call this HINTING.

Now that you're all up to speed on what the possibilities are, let 
me remind you of the debate.  It was offered to the net as fact that 
only POLLING could provide anything resembling good performance.  
I countered that in most situations, a synchronous, compressing version 
of MOTION could work well, but that HINTING was the one true path to 
enlightenment.  I asserted that HINTING and POLLING would be 
indistinguishable, except that HINTING would use fewer resources.

So what did I do?  First, I implemented each of these strategies 
as best I could.  Then, I wrapped a program around this that creates 
two windows, randomly choosing the interaction style for each window.  
In each case, the user interface was fixed: you press the left mouse 
button in a window, and hold it down.  As you move the mouse, the 
outline of a rectangle is displayed in the window.  If you press 
another button while the left button is down, or release the left 
button, the rectangle vanishes.  When you think that you've detected 
a difference in the performance of the two windows, you middle-click 
in the window you "like better", or at random if you're sure they're 
different, but not sure which was better.  If you're convinced that 
they are, in fact, identical (which is one of the possibilities), 
you right-click in either window.

To refute my hypothesis that HINTING and POLLING were indistinguishable, I 
didn't need to put in the stuff about expressing a preference.  But I 
thought it might be interesting, and it didn't seem to compromise the 
experiment much.  I did put in this statement when I asked people to run 
my program:

    Don't feel that you have to choose between them if you feel they're 
    the same; part of the experiment includes sometimes making them 
    actually *be* the same, so that I can tell if the feeling that 
    they're different is real or not.  In fact, I'm more interested 
    in whether you press the middle button or the right button than 
    I am in which window you liked better, unless it turns out that 
    there are very clear preferences on that score.  What I'll be 
    doing is looking to see if the frequency with which people could 
    tell the difference between two styles of dragging is statistically 
    distinguishable from the frequency with which they perceived 
    differences between identical styles.

In order to enhance any perception of differences, the program prints 
out what interaction style each window has as soon as the user presses 
the middle or right button.  I did ask subjects to hide any 
load-average monitoring tools that they might have on their screen, 
so that they would not observe that side-effect of polling.  I felt 
that telling the user what style of interaction each window had would 
allow users with "golden eyes" to hone their skills at finding ways 
to establish the kind of window they were dealing with, which might 
help them in refuting my hypothesis of indistinguishability.  It does 
mean that the preference information is less valuable than it might 
be, assuming that there are techniques for distinguishing, since users 
who are good at distinguishing, and have a preference for some style 
could feign a preference for that style.  I considered this an 
acceptable compromise in the design of the experiment, after removing 
myself from the subject pool.

The implementations of the different styles were relative naive.  
In particular, the polling loop always polls; a more sophisticated 
implementation might choose to do something else after a few 
round-trips with no detected motion.  The motion loop synchronizes 
after every painting request; a more sophisticated implementation 
might try to synchronize only after every fifth or tenth request.  
I don't think either of these affected the outcome much, but you should 
feel free to rerun the experiment however you like. 

I released the program, and captured results for six weeks.  There 
are three obvious conclusions from the data:

1) In our environment (fast workstations), this experiment was not 
   sensitive enough to exhibit a difference between the tracking 
   styles, when the program and the server ran on the same 
   workstation.  That is, the hypothesis of indistinguishibility may 
   hold in the local-execution case.

2) When running over a local-area network, using a time-shared machine 
   to run the client, significance was achieved in determining whether 
   the two windows were identical or not.

3) No distinction was observed in preference for one style over another.
   Again, due to the design of the experiment, this conclusion is at 
   best weakly supported by the evidence.


The data:

                                Local                 Remote
               Perceived as:  same  different       same  different

Both POLLING or HINTING         8       4            17      14
One POLLING, one HINTING        7       3             6      15

Both POLLING or MOTION          9       4            11       6
One POLLING, one MOTION         2       2            15      15

Both HINTING or MOTION          7       4            10       8
One HINTING, one MOTION         9       4            10      23

The local case is client and server on the same workstation, remote 
is client on a time-shared machine on the same local ethernet.  A 
limited number of experiments were run across gateways, and over 
56 kilobaud serial lines, with similar results.  In the remote case, 
of the 30 trials where POLLING went head-to-head with another style, 
and was detected as different, it was preferred 15 times.  Of the 
38 remote trials each where HINTING and MOTION went head-to-head 
with another style, each was preferred 19 times.  In the local case, 
POLLING was favored in all five of the head-to-head trials in which 
a distinction was found; HINTING was favored in 4 of 7 head-to-head 
trials; MOTION was favored in 1 of 6 head-to-head trials.  The 
cumulative statistics in the local case show that in exactly 1/3 
of all cases, whether a distinction exists or not, a distinction 
was perceived.  The remote case shows a strong bias toward detecting 
differences; of the 84 trials in which the windows differed, the 
difference was noticed 53 times (63%).  Of the 32 times that the 
windows were the same, a difference was perceived 14 times (44%).

Mark

#! /bin/sh
# This is a shell archive.  If you save it in file 'foo', unpack it
# by typing 'sh foo'
echo Extracting Makefile
cat >Makefile <<'!End!of!file!'
#CFLAGS = -DMAILTO=user@host.domain

abx: abx.o
	$(CC) -o abx abx.o -lX11
!End!of!file!
echo Extracting abx.c
cat >abx.c <<'!End!of!file!'
#include <sys/types.h>
#include <sys/timeb.h>
#include <stdio.h>
#include <X11/Xlib.h>
#include <X11/X.h>
#include <signal.h>
#include <pwd.h>

extern long random();
extern char *getlogin(), *getenv();

typedef struct {
	int h_x;
	int h_y;
	int h_height;
	int h_width;
	int highlightOn;
} HighlightInfoRec, *HighlightInfo;

HighlightInfoRec nhi, ohi;

Display *dpy;
GC xorGC;
Window w0, w1;
int c0, c1;
char *testname[] ={"Poll", "Hint", "Motion"};
char *preference[] = {"chose window 0", "chose window 1", "didn't choose"};
int hist[3][3][3];

cleanup(sig, code, scp) int sig, code; struct sigcontext *scp; {
  int i,j,k;
  FILE *f;
  char *unam, hostbuf[1000];
  struct passwd *pwent;

#ifdef MAILTO
  f = popen("mail MAILTO", "w");
  unam = getlogin();
  if (!unam || !*unam)
    unam = getpwuid(getuid())->pw_name;
  if (!unam)
    unam = "Don't know who";
  gethostname(hostbuf, 999);
  hostbuf[999] = '\0';
  fprintf(f, "%s on %s, talking to display %s\n", unam, hostbuf,
	  getenv("DISPLAY"));
  for (i=0; i<3; i++)
    for (j=0; j<3; j++)
      for (k=0; k<3; k++)
	fprintf(f, "%d%c", hist[i][j][k], k<2?' ':'\n');
  pclose(f);
#endif
  exit(0);
}

main(argc, argv) char *argv[]; {
  int i,j,k,c2;
  FILE *f;
  XGCValues gcv;
  time_t now = time((long *)0);
  int white, black;

  if(!(dpy = XOpenDisplay(NULL))) {
    fprintf(stderr, "Can't open display!\n");
    exit(1);
  }
  signal(SIGINT, cleanup);
  for (i=0; i<3; i++)
    for (j=0; j<3; j++)
      for (k=0; k<3; k++)
	hist[i][j][k] = 0;
  white = WhitePixel(dpy,  DefaultScreen(dpy));
  black = BlackPixel(dpy,  DefaultScreen(dpy));
  gcv.function = GXinvert;
  gcv.plane_mask = black ^ white;
  xorGC = XCreateGC(dpy, DefaultRootWindow(dpy), GCFunction | GCPlaneMask, 
		    &gcv);
  srandom(getpid() + now);
  printf("Left drag in windows to test dragging.\n");
  printf("Middle click in a window to express a preference for that window,\n"
	 );
  printf("if you can tell them apart; if you can tell them apart, but had no\n"
	 );
  printf("preference, middle click at random.\n");
  printf("Right click in either window if they are indistinguishable.\n");
  printf("Type ^C to exit.\n");
  w0 = XCreateSimpleWindow(dpy, DefaultRootWindow(dpy), 0, 0, 200, 200, 0,
			   black,  white);
  w1 = XCreateSimpleWindow(dpy, DefaultRootWindow(dpy), 200, 0, 200, 200, 0,
			   black,  white);
  XStoreName(dpy, w0, "Test of drag styles, window 0");
  XStoreName(dpy, w1, "Test of drag styles, window 1");
  XSetIconName(dpy, w0, "Drag test 0");
  XSetIconName(dpy, w1, "Drag test 1");
  XMapWindow(dpy, w0);
  XMapWindow(dpy, w1);
  for (;;) {
    c0 = random() % 3;
    c1 = random() % 3;
    c2 = runtest();
    printf("Window 0 was %s.  Window 1 was %s.  You %s.\n",
     testname[c0], testname[c1], preference[c2]);
    hist[c0][c1][c2]++;
    fflush(stdout);
  }
}

runtest() {
  XEvent ev;
  Window jroot, jchild;
  int rx, ry, x, y, state, chorded;

  nhi.highlightOn = False;
  ohi = nhi;
  switch(c0) {
    case 0: XSelectInput(dpy, w0, ButtonPressMask | ButtonReleaseMask);
            break;
    case 1: XSelectInput(dpy, w0, ButtonPressMask | ButtonReleaseMask |
			 Button1MotionMask | PointerMotionHintMask);
            break;
    case 2: XSelectInput(dpy, w0, ButtonPressMask | ButtonReleaseMask |
			 Button1MotionMask);
    }
  switch(c1) {
    case 0: XSelectInput(dpy, w1, ButtonPressMask | ButtonReleaseMask);
            break;
    case 1: XSelectInput(dpy, w1, ButtonPressMask | ButtonReleaseMask |
			 Button1MotionMask | PointerMotionHintMask);
            break;
    case 2: XSelectInput(dpy, w1, ButtonPressMask | ButtonReleaseMask |
			 Button1MotionMask);
    }
  for(;;) {
    XNextEvent(dpy, &ev);
    switch (ev.xany.type) {
    case ButtonPress:
      if (chorded = !!(ev.xbutton.state & ~LockMask)) {
	nhi.highlightOn = False;
	UndrawAndDrawOutline(ev.xbutton.window, &ohi, &nhi);
	ohi = nhi;
	break;
      }
      if (ev.xbutton.button != Button1) break;
      nhi.highlightOn = True;
      nhi.h_x = ev.xbutton.x;
      nhi.h_y = ev.xbutton.y;
      nhi.h_width = nhi.h_height = 0;
      UndrawAndDrawOutline(ev.xbutton.window, &ohi, &nhi);
      ohi = nhi;
      if (ev.xbutton.window == w0 && c0 == 0 ||
	  ev.xbutton.window == w1 && c1 == 0)
	while (! XEventsQueued(dpy, QueuedAfterReading)) {
	  XQueryPointer(dpy, ev.xbutton.window, &jroot, &jchild,
			&rx, &ry, &x, &y, &state);
	  RedrawOutline(ev.xbutton.window, x, y);
	}
      break;
    case ButtonRelease:
      if (chorded) break;
      switch(ev.xbutton.button) {
      case Button1:
	nhi.highlightOn = False;
	UndrawAndDrawOutline(ev.xbutton.window, &ohi, &nhi);
	ohi = nhi;
	break;
      case Button2:
	return ev.xbutton.window == w1;
      case Button3:
	return 2;
      }
      break;
    case MotionNotify:
        if (ev.xmotion.window == w0 && c0 == 1 ||
	    ev.xmotion.window == w1 && c1 == 1) {
	  RedrawOutline(ev.xmotion.window, ev.xmotion.x, ev.xmotion.y);
	  XQueryPointer(dpy, ev.xmotion.window, &jroot, &jchild,
			&rx, &ry, &x, &y, &state);
	  RedrawOutline(ev.xmotion.window, x, y);
	} else {
	  XEvent peek;
	  while (XEventsQueued(dpy, QueuedAfterReading) &&
		 (XPeekEvent(dpy, &peek),
		  peek.xany.type == MotionNotify &&
		  peek.xmotion.window == ev.xmotion.window))
	    XNextEvent(dpy, &ev);
	  RedrawOutline(ev.xmotion.window, ev.xmotion.x, ev.xmotion.y);
	  XSync(dpy, False);
	}
      break;
    }
  }
}

RedrawOutline(w, x, y) Window w; int x,y; {
  nhi.h_width = x - nhi.h_x;
  nhi.h_height = y - nhi.h_y;
  UndrawAndDrawOutline(w, &ohi, &nhi);
  ohi = nhi;
}

static XSegment *
ComputeOutline(h, q)
     HighlightInfo h; register XSegment *q;
{

  if (!(h->highlightOn)) return q;
  
  if (h->h_width < 0) { h->h_x += h->h_width; h->h_width = -h->h_width; }

  if (h->h_height < 0) { h->h_y += h->h_height; h->h_height = -h->h_height; }

  q->x1 = h->h_x;
  q->y1 = h->h_y;
  q->x2 = h->h_x + h->h_width - 1;
  q++->y2 = h->h_y;

  q->x1 = h->h_x + h->h_width - 1;
  q->y1 = h->h_y + 1;
  q->x2 = h->h_x + h->h_width - 1;
  q++->y2 = h->h_y + h->h_height - 2;

  q->x1 = h->h_x + h->h_width - 1;
  q->y1 = h->h_y + h->h_height - 1;
  q->x2 = h->h_x;
  q++->y2 = h->h_y + h->h_height - 1;

  q->x1 = h->h_x;
  q->y1 = h->h_y + h->h_height - 2;
  q->x2 = h->h_x;
  q++->y2 = h->h_y + 1;

  return q;
}

UndrawAndDrawOutline(w, ohi, nhi) Window w; HighlightInfo ohi, nhi;
{
  XSegment outline[8], *r;

  if (ohi->highlightOn == nhi->highlightOn && ohi->h_x == nhi->h_x
      && ohi->h_y == nhi->h_y && ohi->h_width == nhi->h_width
      && ohi->h_height == nhi->h_height)
    return;
  r = ComputeOutline(nhi, ComputeOutline(ohi, outline));
  if (r != outline) XDrawSegments(dpy, w, xorGC, outline, r-outline);
}
!End!of!file!