[Openmcl-devel] Re: Hemlock performance investigation

Gary Byers gb at clozure.com
Sat Aug 28 23:48:45 PDT 2004

On Sat, 28 Aug 2004, alex crain wrote:

> On Aug 28, 2004, at 12:00 PM, Hamilton Link wrote:
> > OK, the following addition by Gary (can't hardly go to bed late enough
> > or get up early enough to keep up with that guy) makes layout WAY
> > faster, when the file is opened and just in general. So typing is
> > lickety-split now and maybe Gary won't have to get coffee when hemlock
> > is opening large files any more. Thanks Gary and thanks to all that
> > provided quality pointers into the Cocoa documentation (he says, as he
> > turns around and does a random insertion of said material into his in
> > box).
> Hey kids.
> I've been following the discussion but I'm on vacation and I'm hacked
> into a random Wifi network with a very weak signal so I'm mostly
> offline and haven't been responding.  (I have to sit in a certain spot
> on the porch of my hotel - it's probably for the best)
> Garys solution certainly works well enough interactive use but I think
> that there is an opportunity for improvement. I've been digging, and it
> looks like the fundamental issue is that NSLayoutManager is trying to
> layout ~3000 glyphs for every insertion. This strikes me as
> particularly unbright since there are only 1920 visible characters on a
> 24x80 display and
> a more typical number is ~800 visible characters.

Sometimes, the layout manager seems to be more interested in where
lines start and end than in getting every character for other purposes.
HEMLOCK-BUFFER-STRINGs can implement a version of the NSString method
getLineStart:end:contentsEnd:forRange: that doesn't need to look at
the actual characters:

(define-objc-method ((:void :get-line-start ((:* :unsigned) startptr)
                            :end ((:* :unsigned) endptr)
                            :contents-end ((:* :unsigned) contents-endptr)
                            :for-range (:<NSR>ange r))
  (let* ((cache (hemlock-buffer-string-cache self))
         (index (pref r :<NSR>ange.location))
         (length (pref r :<NSR>ange.length))
	  (hi::buffer-gap-context (buffer-cache-buffer cache))))
    (update-line-cache-for-index cache index)
    (unless (%null-ptr-p startptr)
      ;; Index of the first character in the line which contains
      ;; the start of the range.
      (setf (pref startptr :unsigned)
            (buffer-cache-workline-offset cache)))
    (unless (%null-ptr-p endptr)
      ;; Index of the newline which terminates the line which
      ;; contains the start of the range.
      (setf (pref endptr :unsigned)
            (+ (buffer-cache-workline-offset cache)
               (buffer-cache-workline-length cache))))
    (unless (%null-ptr-p contents-endptr)
      ;; Index of the first character of the first line past
      ;; the end of the range.
      (unless (zerop length)
        (update-line-cache-for-index cache (+ index length)))
      (setf (pref contents-endptr :unsigned)
            (1+ (+ (buffer-cache-workline-offset cache)
                   (buffer-cache-workline-length cache)))))))

With that method in place, we should see significantly fewer calls to
getCharacters:range: (and, after Andrew's suggestion, should rarely
be seeing characterAtIndex: anymore.)

We're probably still seeing more getCharacters:range: calls after an
insertion than we expect to see.  Hamilton and I had sent a few emails
back and forth over the last few days; he was generally seeing that
the number of characterAtIndex: calls after a modification was
proportional to the size of the buffer, and I was seeing something
proportional to the number of visible characters.  (Neither of these
is optimal, and it's hard to know how many of these calls were looking
for line breaks and how many were done for other reasons.)

We experimented with disabling background layout, and that seemed to
significantly reduce the number of calls that Hamilton was seeing.  At
that time (just before Andrew's reminder that implementing
getCharacters:range: was possible and desirable) I'd developed a theory:

1) the text system will display the view before it's completely laid
out.  While the user's admiring the pretty window (or getting coffee),
the layout manager makes one or more complete passes through the buffer,
calculating hard (and soft, if wrapping is enabled) line breaks and
caching glyphs.  I believe that I was seeing a ratio of character:atIndex:
calls to buffer size of almost exactly 4:1; I don't know whether this
involved 4 complete passes through the buffer or not, but it took
several seconds on a large (~350K) buffer.

2) If the user started typing before that background layout was complete,
it had to start all over again, and it might be possible to get the
layout manager in a state where it's always doing background layout (or
otherwise behaving hysterically.)

Disabling background layout entirely (which probably isn't a good idea)
seemed to reduce this hysteria, and Hamilton and I were both looking
at how to do so when Andrew mentioned getCharacters:range: (which
reduces the cost of being hysterical signficantly.)

The moral of the story (if there is one) is that what the layout
manager does in response to a change may depend on how up-to-date and
consistent things are when the change is made; at least some of the
character-fetching activity that we see in apparent response to an
editing change may (intentionally and correctly) have less to do with
that change than with some unrelated work that the layout manager's
been meaning to get around to one of these days ...

> Interesting - If the file is > ~3500 characters, NSLayoutManager does
> not try to layout the entire file, but does layout more then the
> visible area. I'm thinking that it's working with a virtual screen
> behind the scrollbars and is checking to see if inserting the new glyph
> changes the position of any of the other displayed glyphs.

I think that the layout manager sees its job as keeping one or more
NSTextContainers updated.  I'm not sure to what degree it's even aware
of the fact that the text container is embedded in a view hierarchy that
imposes clipping, or that it's any more concerned about the visible
portions of the container than other portions.

> Now, we know something that NSLayoutManager does not know, in that we
> are using a fixed width font. It seems likely that this will always be
> the case, as working with variable fonts in a programming editor is
> evil, so I've been looking for a way to explain that to the layout
> manager  so that it will simply shift all the glyphs on a line over one
> and do the insertion.

Using a subclass of NSLayoutManager (and maybe some flavor of NSTypeSetter)
might enable us to optimize the fixed-width font case(s).

I'm tempted to think that if suboptimal layout manager behavior isn't
killing us anymore, this is less important than it might have seemed
a few days ago.

A lisp-aware NSLayoutManager might be good for other reasons; background
layout might be able to do syntax highlighting or other "sectionization"
activities.  (I'm not sure that this is the right place for that, but
it might be.)

> The good news is that I managed to get it to work. That bad news is
> that I was working in a dynamic environment and I can't remember
> exactly what I did to make it work. I did, however
> see some behavior where I was editing a 14K file and each insertions
> was generating about eight calls to :character-at-position and six to
> :get-characters.
> I'm going back to my digging, but if anyone has any suggestions I'd
> like to hear them.

I'd say that if further improvements to general responsiveness should
probably be pursued if they're low-hanging fruit, but "general
responsiveness" is probably less of a negative than it was a few days
ago.  (Being able to type quicker means that we'll run into all of
the other broken/missing functionality faster.)

> :alex
> _______________________________________________
> Openmcl-devel mailing list
> Openmcl-devel at clozure.com
> http://clozure.com/mailman/listinfo/openmcl-devel

More information about the Openmcl-devel mailing list