xpra icon
Bug tracker and wiki

This bug tracker and wiki are being discontinued
please use https://github.com/Xpra-org/xpra instead.


Ticket #23: adaptive-damage.patch

File adaptive-damage.patch, 14.9 KB (added by Antoine Martin, 10 years ago)

better batching: send whole screen to save rectangles and speedup/slow down according to client consumption rate

  • xpra/server.py

     
    131131gobject.type_register(DesktopManager)
    132132
    133133class ServerSource(object):
     134   
     135    BATCH_EVENTS = True
     136    MAX_PIXELS = 800*600*25     #small screen at 25 frames
     137    MAX_EVENTS = 30             #maximum number of damage events
     138    TIME_UNIT = 1               #per second
     139    INITIAL_BATCH_DELAY = 100    #how long to batch updates for (in millis)
     140    MIN_BATCH_DELAY = 50
     141    MAX_BATCH_DELAY = 500
     142   
    134143    # Strategy: if we have ordinary packets to send, send those.
    135144    # When we don't, then send window updates (expired ones first).
    136     def __init__(self, protocol, encoding, mmap, mmap_size):
     145    def __init__(self, protocol, encoding, damage_sequence, mmap, mmap_size, desktop_manager):
    137146        self._ordinary_packets = []
    138147        self._protocol = protocol
    139148        self._encoding = encoding
     
    141150        self._damage_last_events = {}
    142151        self._damage_delayed = {}
    143152        self._damage_delayed_expired = {}
     153        self._damage_sequence = damage_sequence
     154        self.batch_delay = ServerSource.INITIAL_BATCH_DELAY
     155
     156        self.last_client_sequence = -1
     157        self._sequence = 0
    144158        self._mmap = mmap
    145159        self._mmap_size = mmap_size
     160        self._desktop_manager = desktop_manager
    146161        protocol.source = self
    147162        if self._have_more():
    148163            protocol.source_has_more()
     
    166181                del d[id]
    167182
    168183    def damage(self, id, window, x, y, w, h):
    169         BATCH_EVENTS = True
    170         MAX_PIXELS = 800*600*25 #small screen at 25 frames
    171         MAX_EVENTS = 30         #maximum number of damage events
    172         TIME_UNIT = 1           #per second
    173         BATCH_DELAY = 50        #how long to batch updates for (in millis)
    174         def add_damage_to_region(region):
    175             region.union_with_rect(gtk.gdk.Rectangle(x, y, w, h))
     184        def maybe_send_full_window():
     185            (_, _, ww, wh) = self._desktop_manager.window_geometry(window)
     186            if ww*wh<=(512+w*h*1.2):
     187                return ww,wh
     188            return None
     189        def add_damage_to_region(region, damage_info):
     190            full_window_update = damage_info[0]
     191            if full_window_update is None:
     192                now_full = maybe_send_full_window()
     193                if now_full is None:
     194                    region.union_with_rect(gtk.gdk.Rectangle(x, y, w, h))
     195                else:
     196                    damage_info[0] = now_full
    176197        def damage_now():
    177             log("damage(%s, %s, %s, %s, %s)", id, x, y, w, h)
    178             _, region = self._damage.setdefault(id, (window, gtk.gdk.Region()))
    179             add_damage_to_region(region)
     198            log.info("damage(%s, %s, %s, %s, %s) sending now with sequence %s", id, x, y, w, h, self._sequence)
     199            _, region, damage_info, _ = self._damage.setdefault(id, (window, gtk.gdk.Region(), [None], self._sequence))
     200            self._sequence += 1
     201            add_damage_to_region(region, damage_info)
    180202            self._protocol.source_has_more()
    181         if not BATCH_EVENTS:
     203        if not ServerSource.BATCH_EVENTS:
    182204            return damage_now()
    183205
    184206        #record this damage event in the damage_last_events queue:
    185207        now = time.time()
    186         last_events = self._damage_last_events.setdefault(id, deque(maxlen=MAX_EVENTS))
     208        last_events = self._damage_last_events.setdefault(id, deque(maxlen=ServerSource.MAX_EVENTS))
    187209        last_events.append((now, w*h))
    188210
    189211        delayed = self._damage_delayed.get(id)
    190212        if delayed:
    191             (window, region) = delayed
    192             add_damage_to_region(region)
    193             log("damage(%s, %s, %s, %s, %s) using existing delayed region: %s", id, x, y, w, h, delayed)
     213            (_, region, damage_info, _) = delayed
     214            add_damage_to_region(region, damage_info)
     215            log.info("damage(%s, %s, %s, %s, %s) using existing delayed region: %s", id, x, y, w, h, delayed)
    194216            return
    195217
    196         pixel_count = 0
    197         for last_time,pixels in last_events:
    198             pixel_count += pixels
    199             if pixel_count>=MAX_PIXELS:
    200                 break
    201         if pixel_count>=MAX_PIXELS:
    202             log.info("damage(%s, %s, %s, %s, %s) pixel storm: %s pixels in %s, batching", id, x, y, w, h, pixel_count, (now-last_time))
     218        if self.last_client_sequence>=0 and (self._sequence-self.last_client_sequence)>3:
     219            #client is struggling to keep up!
     220            self.batch_delay = min(ServerSource.MAX_BATCH_DELAY, self.batch_delay*2)
     221            log.info("damage(%s, %s, %s, %s, %s) client is not keeping up, increased batch delay to: %s", id, x, y, w, h, self.batch_delay)
    203222        else:
    204             if len(last_events)<MAX_EVENTS:
    205                 log("damage(%s, %s, %s, %s, %s) recent event list is too small, not batching", id, x, y, w, h)
    206                 return damage_now()
    207             when,_ = last_events[0]
    208             if now-when>TIME_UNIT:
    209                 log("damage(%s, %s, %s, %s, %s) damage events are outside the batching time limit, not batching", id, x, y, w, h)
    210                 return damage_now()
     223            #if client was struggling previously, reduce batch delay:
     224            if self.batch_delay>ServerSource.MIN_BATCH_DELAY:
     225                self.batch_delay = max(ServerSource.MIN_BATCH_DELAY, int(self.batch_delay*2/3))
     226                log.info("damage(%s, %s, %s, %s, %s) client is keeping up, reduced batch delay to: %s", id, x, y, w, h, self.batch_delay)
    211227
     228            pixel_count = 0
     229            for last_time,pixels in last_events:
     230                pixel_count += pixels
     231                if pixel_count>=ServerSource.MAX_PIXELS:
     232                    break
     233            if pixel_count>=ServerSource.MAX_PIXELS:
     234                log.info("damage(%s, %s, %s, %s, %s) pixel storm: %s pixels in %s, batching", id, x, y, w, h, pixel_count, (now-last_time))
     235            else:
     236                if len(last_events)<ServerSource.MAX_EVENTS:
     237                    log.info("damage(%s, %s, %s, %s, %s) recent event list is too small, not batching", id, x, y, w, h)
     238                    return damage_now()
     239                when,_ = last_events[0]
     240                if now-when>ServerSource.TIME_UNIT:
     241                    log.info("damage(%s, %s, %s, %s, %s) damage events are outside the batching time limit, not batching", id, x, y, w, h)
     242                    return damage_now()
     243
    212244        #create a new delayed region:
    213245        region = gtk.gdk.Region()
    214         add_damage_to_region(region)
    215         self._damage_delayed[id] = (window, region)
     246        damage_info = [None]
     247        add_damage_to_region(region, damage_info)
     248        self._damage_delayed[id] = (window, region, damage_info, self._sequence)
     249        self._sequence += 1
    216250        def send_delayed():
    217251            """ move the delayed rectangles to the expired list """
    218             log("send_delayed for %s ", id)
     252            log("send_delayed for %s", id)
    219253            delayed = self._damage_delayed.get(id)
    220254            if delayed:
    221255                del self._damage_delayed[id]
    222256                self._damage_delayed_expired[id] = delayed
     257                log.info("moving region %s to expired list", delayed)
    223258                self._protocol.source_has_more()
    224259            return False
    225         log("damage(%s, %s, %s, %s, %s) scheduling batching expiry", id, x, y, w, h)
    226         gobject.timeout_add(BATCH_DELAY, send_delayed)
     260        log.info("damage(%s, %s, %s, %s, %s) scheduling batching expiry for sequence %s in %sms", id, x, y, w, h, self._sequence, self.batch_delay)
     261        gobject.timeout_add(self.batch_delay, send_delayed)
    227262
    228263    def next_packet(self):
    229264        if self._ordinary_packets:
     
    240275        else:
    241276            damage_dict = self._damage
    242277        item = damage_dict.items()[0]
    243         id, (window, damage) = item
    244         (x, y, w, h) = get_rectangle_from_region(damage)
    245         rect = gtk.gdk.Rectangle(x, y, w, h)
    246         damage.subtract(gtk.gdk.region_rectangle(rect))
    247         if damage.empty():
     278        id, (window, damage, damage_info, sequence) = item
     279        full_window = damage_info[0]
     280        if full_window is not None:
     281            x, y = 0, 0
     282            w, h = full_window
    248283            del damage_dict[id]
     284        else:
     285            try:
     286                (x, y, w, h) = get_rectangle_from_region(damage)
     287            except ValueError:
     288                log.error("next_damage_packet is empty: %s", damage)
     289                del damage_dict[id]
     290                return None
     291            if damage:
     292                rect = gtk.gdk.Rectangle(x, y, w, h)
     293                damage.subtract(gtk.gdk.region_rectangle(rect))
     294                log.info("next_damage_packet for sequence %s: %s, remains: %s", sequence, rect, damage)
     295                if damage.empty():
     296                    del damage_dict[id]
    249297        # It's important to acknowledge changes *before* we extract them,
    250298        # to avoid a race condition.
    251         window.acknowledge_changes(x, y, w, h)
     299        window.acknowledge_changes()
    252300        pixmap = window.get_property("client-contents")
    253301        if pixmap is None:
    254302            log.error("wtf, pixmap is None?")
    255303            return  None
     304        if full_window is not None:
     305            log.info("next_damage_packet sending full window: %s", pixmap.get_size())
     306            w, h = pixmap.get_size()
    256307        (x2, y2, w2, h2, coding, data) = self._get_rgb_data(pixmap, x, y, w, h)
    257308        if not w2 or not h2:
    258309            return None
     
    287338                log("sending damage with mmap: %s", data)
    288339            else:
    289340                log("mmap area full... ouch!")
    290         return ["draw", id, x2, y2, w2, h2, coding, data]
     341        packet = ["draw", id, x2, y2, w2, h2, coding, data]
     342        if self._damage_sequence:
     343            packet.append(sequence)
     344        return packet
    291345
    292346    def _get_rgb_data(self, pixmap, x, y, width, height):
    293347        pixmap_w, pixmap_h = pixmap.get_size()
     
    397451        self.mmap = None
    398452        self.mmap_size = 0
    399453
     454        self.damage_sequence = False
    400455        self.send_notifications = False
    401456        self.last_cursor_serial = None
    402457        self.cursor_image = None
     
    10691124        # set this as our new one:
    10701125        if self._protocol is not None:
    10711126            self.disconnect("new valid connection received")
     1127        self.damage_sequence = capabilities.get("damage_sequence", False)
    10721128        #if "encodings" not specified, use pre v0.0.7.26 default: rgb24
    10731129        self.encodings = capabilities.get("encodings", ["rgb24"])
    10741130        self._set_encoding(capabilities.get("encoding", None))
     
    10831139            self.mmap = mmap.mmap(file.fileno(), self.mmap_size)
    10841140            log.info("using client supplied mmap file=%s, size=%s", mmap_file, self.mmap_size)
    10851141        self._protocol = proto
    1086         self._server_source = ServerSource(self._protocol, self.encoding, self.mmap, self.mmap_size)
     1142        self._server_source = ServerSource(self._protocol, self.encoding, self.damage_sequence, self.mmap, self.mmap_size, self._desktop_manager)
    10871143        # do screen size calculations/modifications:
    10881144        self.send_hello(capabilities)
    10891145        if "deflate" in capabilities:
     
    11541210        capabilities["encodings"] = ENCODINGS
    11551211        capabilities["encoding"] = self.encoding
    11561212        capabilities["resize_screen"] = self.randr
     1213        if client_capabilities.get("damage_sequence", False):
     1214            capabilities["damage_sequence"] = True
    11571215        if "key_repeat" in client_capabilities:
    11581216            capabilities["key_repeat"] = client_capabilities.get("key_repeat")
    11591217        if self.session_name:
     
    12441302            self._damage(window, 0, 0, w, h)
    12451303        (x, y, _, _) = self._desktop_manager.window_geometry(window)
    12461304        self._desktop_manager.configure_window(window, x, y, w, h)
     1305        (_, _, ww, wh) = self._desktop_manager.window_geometry(window)
     1306        log.info("resize_window to %sx%s, desktop manager set it to %sx%s", w, h, ww, wh)
    12471307
    12481308    def _process_focus(self, proto, packet):
    12491309        if len(packet)==3:
     
    13621422        log.info("Shutting down in response to request")
    13631423        self.quit(False)
    13641424
     1425    def _process_damage_sequence(self, proto, packet):
     1426        (_, sequence) = packet
     1427        log("received sequence: %s", sequence)
     1428        self._server_source.last_client_sequence = sequence
     1429
    13651430    def _process_buffer_refresh(self, proto, packet):
    13661431        (_, id, _, jpeg_qual) = packet
    13671432        if self.encoding=="jpeg":
     
    14191484        "close-window": _process_close_window,
    14201485        "shutdown-server": _process_shutdown_server,
    14211486        "jpeg-quality": _process_jpeg_quality,
     1487        "damage-sequence": _process_damage_sequence,
    14221488        "buffer-refresh": _process_buffer_refresh,
    14231489        "desktop_size": _process_desktop_size,
    14241490        "encoding": _process_encoding,
  • xpra/client.py

     
    360360        self.notifications_enabled = True
    361361        self._client_extras = ClientExtras(self, opts)
    362362        self.clipboard_enabled = opts.clipboard and self._client_extras.supports_clipboard()
     363        self.damage_sequence = False
    363364        self.mmap_enabled = False
    364365        self.supports_mmap = opts.mmap and self._client_extras.supports_mmap()
    365366        self.mmap = None
     
    651652        root_w, root_h = gtk.gdk.get_default_root_window().get_size()
    652653        capabilities_request["desktop_size"] = [root_w, root_h]
    653654        capabilities_request["png_window_icons"] = True
     655        capabilities_request["damage_sequence"] = True
    654656        key_repeat = self._client_extras.get_keyboard_repeat()
    655657        if key_repeat:
    656658            capabilities_request["key_repeat"] = key_repeat
     
    741743        self.notifications_enabled = capabilities.get("notifications", False)
    742744        clipboard_server_support = capabilities.get("clipboard", True)
    743745        self.clipboard_enabled = clipboard_server_support and self._client_extras.supports_clipboard()
     746        self.damage_sequence = capabilities.get("damage_sequence", False)
    744747        self.mmap_enabled = self.supports_mmap and self.mmap_file and capabilities.get("mmap_enabled")
    745748        if self.mmap_enabled:
    746749            log.info("mmap enabled using %s", self.mmap_file)
     
    781784        self._process_new_common(packet, True)
    782785
    783786    def _process_draw(self, packet):
    784         (_, id, x, y, width, height, coding, data) = packet
     787        (id, x, y, width, height, coding, data) = packet[1:8]
     788        if len(packet)==9:
     789            sequence = packet[8]
     790            log.info("process_draw: sequence=%s", sequence)
     791        else:
     792            sequence = None
    785793        window = self._id_to_window[id]
    786794        window.draw(x, y, width, height, coding, data)
     795        if sequence and self.damage_sequence:
     796            self.send(["damage-sequence", sequence])
    787797
    788798    def _process_cursor(self, packet):
    789799        (_, new_cursor) = packet