From f3788e3c7881de25c7d699bb9940b3cbd4dc3682 Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Wed, 17 Dec 2014 23:37:08 +0000 Subject: [PATCH 01/36] Test some ideas that might help performance a bit --- synapse/handlers/federation.py | 4 ++-- synapse/storage/_base.py | 34 ++++++++++++++++++++++++++++------ synapse/storage/roommember.py | 8 +------- synapse/storage/state.py | 9 +-------- 4 files changed, 32 insertions(+), 23 deletions(-) diff --git a/synapse/handlers/federation.py b/synapse/handlers/federation.py index b76dcd98e..2f6036145 100644 --- a/synapse/handlers/federation.py +++ b/synapse/handlers/federation.py @@ -91,7 +91,7 @@ class FederationHandler(BaseHandler): yield run_on_reactor() - yield self.replication_layer.send_pdu(event, destinations) + self.replication_layer.send_pdu(event, destinations) @log_function @defer.inlineCallbacks @@ -527,7 +527,7 @@ class FederationHandler(BaseHandler): event.signatures, ) - yield self.replication_layer.send_pdu(new_pdu, destinations) + self.replication_layer.send_pdu(new_pdu, destinations) state_ids = [e.event_id for e in context.current_state.values()] auth_chain = yield self.store.get_auth_chain(set( diff --git a/synapse/storage/_base.py b/synapse/storage/_base.py index e0d97f440..a6e2e0e2e 100644 --- a/synapse/storage/_base.py +++ b/synapse/storage/_base.py @@ -440,14 +440,29 @@ class SQLBaseStore(object): ) def _get_events_txn(self, txn, event_ids): - events = [] - for e_id in event_ids: - ev = self._get_event_txn(txn, e_id) + if not event_ids: + return [] - if ev: - events.append(ev) + if len(event_ids) > 50: + events = [] + n = 50 + for e_ids in [event_ids[i:i + n] for i in range(0, len(event_ids), n)]: + events.extend(self._get_events_txn(txn, e_ids)) + return events - return events + where_clause = " OR ".join(["e.event_id = ?" for _ in event_ids]) + + sql = ( + "SELECT internal_metadata, json, r.event_id FROM event_json as e " + "LEFT JOIN redactions as r ON e.event_id = r.redacts " + "WHERE %s" + ) % (where_clause,) + + txn.execute(sql, event_ids) + + res = txn.fetchall() + + return [self._get_event_from_row_txn(txn, *r) for r in res] def _get_event_txn(self, txn, event_id, check_redacted=True, get_prev_content=True): @@ -467,6 +482,13 @@ class SQLBaseStore(object): internal_metadata, js, redacted = res + return self._get_event_from_row_txn( + txn, internal_metadata, js, redacted, check_redacted=check_redacted, + get_prev_content=get_prev_content, + ) + + def _get_event_from_row_txn(self, txn, internal_metadata, js, redacted, + check_redacted=True, get_prev_content=True): d = json.loads(js) internal_metadata = json.loads(internal_metadata) diff --git a/synapse/storage/roommember.py b/synapse/storage/roommember.py index 05b275663..4e416c50b 100644 --- a/synapse/storage/roommember.py +++ b/synapse/storage/roommember.py @@ -183,20 +183,14 @@ class RoomMemberStore(SQLBaseStore): ) def _get_members_query_txn(self, txn, where_clause, where_values): - del_sql = ( - "SELECT event_id FROM redactions WHERE redacts = e.event_id " - "LIMIT 1" - ) - sql = ( - "SELECT e.*, (%(redacted)s) AS redacted FROM events as e " + "SELECT e.* FROM events as e " "INNER JOIN room_memberships as m " "ON e.event_id = m.event_id " "INNER JOIN current_state_events as c " "ON m.event_id = c.event_id " "WHERE %(where)s " ) % { - "redacted": del_sql, "where": where_clause, } diff --git a/synapse/storage/state.py b/synapse/storage/state.py index afe3e5ede..ab8090971 100644 --- a/synapse/storage/state.py +++ b/synapse/storage/state.py @@ -62,14 +62,7 @@ class StateStore(SQLBaseStore): keyvalues={"state_group": group}, retcol="event_id", ) - state = [] - for state_id in state_ids: - s = self._get_events_txn( - txn, - [state_id], - ) - if s: - state.extend(s) + state = self._get_events_txn(txn, state_ids) res[group] = state From 24b5d0185357a308c066bae45306a9c78625dba7 Mon Sep 17 00:00:00 2001 From: Mark Haines Date: Mon, 22 Dec 2014 10:16:02 +0000 Subject: [PATCH 02/36] Include version in User-Agent and Server headers --- synapse/http/agent_name.py | 18 ++++++++++++++++++ synapse/http/client.py | 10 ++++++++-- synapse/http/matrixfederationclient.py | 4 +++- synapse/http/server.py | 9 ++++++--- 4 files changed, 35 insertions(+), 6 deletions(-) create mode 100644 synapse/http/agent_name.py diff --git a/synapse/http/agent_name.py b/synapse/http/agent_name.py new file mode 100644 index 000000000..c98024b6a --- /dev/null +++ b/synapse/http/agent_name.py @@ -0,0 +1,18 @@ +# -*- coding: utf-8 -*- +# Copyright 2014 OpenMarket Ltd +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from synapse import __version__ + +AGENT_NAME = ("Synapse/%s" % (__version__,)).encode("ascii") diff --git a/synapse/http/client.py b/synapse/http/client.py index 048a42890..11d6d9cb2 100644 --- a/synapse/http/client.py +++ b/synapse/http/client.py @@ -14,6 +14,7 @@ # limitations under the License. +from synapse.http.agent_name import AGENT_NAME from twisted.internet import defer, reactor from twisted.web.client import ( Agent, readBody, FileBodyProducer, PartialDownloadError @@ -51,7 +52,8 @@ class SimpleHttpClient(object): "POST", uri.encode("ascii"), headers=Headers({ - "Content-Type": ["application/x-www-form-urlencoded"] + b"Content-Type": [b"application/x-www-form-urlencoded"], + b"User-Agent": AGENT_NAME, }), bodyProducer=FileBodyProducer(StringIO(query_bytes)) ) @@ -86,6 +88,9 @@ class SimpleHttpClient(object): response = yield self.agent.request( "GET", uri.encode("ascii"), + headers=Headers({ + b"User-Agent": AGENT_NAME, + }) ) body = yield readBody(response) @@ -108,7 +113,8 @@ class CaptchaServerHttpClient(SimpleHttpClient): url.encode("ascii"), bodyProducer=FileBodyProducer(StringIO(query_bytes)), headers=Headers({ - "Content-Type": ["application/x-www-form-urlencoded"] + b"Content-Type": [b"application/x-www-form-urlencoded"], + b"User-Agent": AGENT_NAME, }) ) diff --git a/synapse/http/matrixfederationclient.py b/synapse/http/matrixfederationclient.py index 8f4db59c7..fc371155a 100644 --- a/synapse/http/matrixfederationclient.py +++ b/synapse/http/matrixfederationclient.py @@ -20,6 +20,7 @@ from twisted.web.client import readBody, _AgentBase, _URI from twisted.web.http_headers import Headers from twisted.web._newclient import ResponseDone +from synapse.http.agent_name import AGENT_NAME from synapse.http.endpoint import matrix_federation_endpoint from synapse.util.async import sleep from synapse.util.logcontext import PreserveLoggingContext @@ -71,6 +72,7 @@ class MatrixFederationHttpClient(object): requests. """ + def __init__(self, hs): self.hs = hs self.signing_key = hs.config.signing_key[0] @@ -83,7 +85,7 @@ class MatrixFederationHttpClient(object): query_bytes=b"", retry_on_dns_fail=True): """ Creates and sends a request to the given url """ - headers_dict[b"User-Agent"] = [b"Synapse"] + headers_dict[b"User-Agent"] = [AGENT_NAME] headers_dict[b"Host"] = [destination] url_bytes = urlparse.urlunparse( diff --git a/synapse/http/server.py b/synapse/http/server.py index f33859cf7..5765dffe3 100644 --- a/synapse/http/server.py +++ b/synapse/http/server.py @@ -14,14 +14,16 @@ # limitations under the License. -from syutil.jsonutil import ( - encode_canonical_json, encode_pretty_printed_json -) +from synapse.http.agent_name import AGENT_NAME from synapse.api.errors import ( cs_exception, SynapseError, CodeMessageException ) from synapse.util.logcontext import LoggingContext +from syutil.jsonutil import ( + encode_canonical_json, encode_pretty_printed_json +) + from twisted.internet import defer, reactor from twisted.web import server, resource from twisted.web.server import NOT_DONE_YET @@ -230,6 +232,7 @@ def respond_with_json_bytes(request, code, json_bytes, send_cors=False, request.setResponseCode(code, message=response_code_message) request.setHeader(b"Content-Type", b"application/json") + request.setHeader(b"Server", AGENT_NAME) if send_cors: request.setHeader("Access-Control-Allow-Origin", "*") From d90e586c858dbd0fffafa41c4fe9efca3559359f Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Wed, 24 Dec 2014 16:56:20 +0000 Subject: [PATCH 03/36] spell out that upgrading is just installing over the top --- README.rst | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/README.rst b/README.rst index f5d2b0af3..87e0d7c63 100644 --- a/README.rst +++ b/README.rst @@ -239,6 +239,11 @@ Upgrading an existing homeserver IMPORTANT: Before upgrading an existing homeserver to a new version, please refer to UPGRADE.rst for any additional instructions. +Otherwise, simply re-install the new codebase over the current one - e.g. +by ``pip install --user --process-dependency-links +https://github.com/matrix-org/synapse/tarball/master`` +if using pip, or by ``git pull`` if running off a git working copy. + Setting up Federation ===================== From 1eb319806b9186d6ddfff3c4516799faa5efbd20 Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Wed, 24 Dec 2014 16:56:32 +0000 Subject: [PATCH 04/36] clarify these instructions a media-repo specific --- docs/media_repository.rst | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/media_repository.rst b/docs/media_repository.rst index e4a697404..1037b5be6 100644 --- a/docs/media_repository.rst +++ b/docs/media_repository.rst @@ -1,6 +1,8 @@ -Media Repository +Media Repository ================ +*Synapse implementation-specific details for the media repository* + The media repository is where attachments and avatar photos are stored. It stores attachment content and thumbnails for media uploaded by local users. It caches attachment content and thumbnails for media uploaded by remote users. From 0e93e01fcb9ca3d0769a52a19527e74724db58b2 Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Wed, 24 Dec 2014 19:45:28 +0000 Subject: [PATCH 05/36] spell out that VoIP needs TURN --- README.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.rst b/README.rst index 87e0d7c63..92b94bcd7 100644 --- a/README.rst +++ b/README.rst @@ -108,6 +108,9 @@ To install the synapse homeserver run:: This installs synapse, along with the libraries it uses, into ``$HOME/.local/lib/`` on Linux or ``$HOME/Library/Python/2.7/lib/`` on OSX. +For reliable VoIP calls to be routed via this homeserver, you MUST configure +a TURN server. See docs/turn-howto.rst for details. + Troubleshooting Installation ---------------------------- From f0128f9600c59fbcb993bccbbbb32486009694d7 Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Tue, 6 Jan 2015 10:55:43 +0000 Subject: [PATCH 06/36] Add RoomMemberStore.get_users_in_room, so that we can get the list of joined users without having to retrieve the full events --- synapse/handlers/room.py | 13 +++++-------- synapse/storage/roommember.py | 13 +++++++++++++ 2 files changed, 18 insertions(+), 8 deletions(-) diff --git a/synapse/handlers/room.py b/synapse/handlers/room.py index deefc3c11..5e5d95add 100644 --- a/synapse/handlers/room.py +++ b/synapse/handlers/room.py @@ -245,14 +245,12 @@ class RoomMemberHandler(BaseHandler): self.distributor.declare("user_left_room") @defer.inlineCallbacks - def get_room_members(self, room_id, membership=Membership.JOIN): + def get_room_members(self, room_id): hs = self.hs - memberships = yield self.store.get_room_members( - room_id=room_id, membership=membership - ) + users = yield self.store.get_users_in_room(room_id) - defer.returnValue([hs.parse_userid(m.user_id) for m in memberships]) + defer.returnValue([hs.parse_userid(u) for u in users]) @defer.inlineCallbacks def fetch_room_distributions_into(self, room_id, localusers=None, @@ -531,11 +529,10 @@ class RoomListHandler(BaseHandler): def get_public_room_list(self): chunk = yield self.store.get_rooms(is_public=True) for room in chunk: - joined_members = yield self.store.get_room_members( + joined_users = yield self.store.get_users_in_room( room_id=room["room_id"], - membership=Membership.JOIN ) - room["num_joined_members"] = len(joined_members) + room["num_joined_members"] = len(joined_users) # FIXME (erikj): START is no longer a valid value defer.returnValue({"start": "START", "end": "END", "chunk": chunk}) diff --git a/synapse/storage/roommember.py b/synapse/storage/roommember.py index 4e416c50b..4921561fc 100644 --- a/synapse/storage/roommember.py +++ b/synapse/storage/roommember.py @@ -123,6 +123,19 @@ class RoomMemberStore(SQLBaseStore): else: return None + def get_users_in_room(self, room_id): + def f(txn): + sql = ( + "SELECT m.user_id FROM room_memberships as m" + " INNER JOIN current_state_events as c" + " ON m.event_id = c.event_id" + " WHERE m.membership = ? AND m.room_id = ?" + ) + + txn.execute(sql, (Membership.JOIN, room_id)) + return [r[0] for r in txn.fetchall()] + return self.runInteraction("get_users_in_room", f) + def get_room_members(self, room_id, membership=None): """Retrieve the current room member list for a room. From d7e8ea67b374d3b006f7277de531302abc410e57 Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Tue, 6 Jan 2015 11:18:02 +0000 Subject: [PATCH 07/36] Reformat --- synapse/storage/_base.py | 27 ++++++++++++++------------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/synapse/storage/_base.py b/synapse/storage/_base.py index a6e2e0e2e..a30b0bc41 100644 --- a/synapse/storage/_base.py +++ b/synapse/storage/_base.py @@ -440,15 +440,16 @@ class SQLBaseStore(object): ) def _get_events_txn(self, txn, event_ids): - if not event_ids: - return [] + if not event_ids: + return [] - if len(event_ids) > 50: - events = [] - n = 50 - for e_ids in [event_ids[i:i + n] for i in range(0, len(event_ids), n)]: - events.extend(self._get_events_txn(txn, e_ids)) - return events + if len(event_ids) > 50: + events = [] + n = 50 + split = [event_ids[i:i + n] for i in range(0, len(event_ids), n)] + for e_ids in split: + events.extend(self._get_events_txn(txn, e_ids)) + return events where_clause = " OR ".join(["e.event_id = ?" for _ in event_ids]) @@ -482,13 +483,13 @@ class SQLBaseStore(object): internal_metadata, js, redacted = res - return self._get_event_from_row_txn( - txn, internal_metadata, js, redacted, check_redacted=check_redacted, - get_prev_content=get_prev_content, - ) + return self._get_event_from_row_txn( + txn, internal_metadata, js, redacted, check_redacted=check_redacted, + get_prev_content=get_prev_content, + ) def _get_event_from_row_txn(self, txn, internal_metadata, js, redacted, - check_redacted=True, get_prev_content=True): + check_redacted=True, get_prev_content=True): d = json.loads(js) internal_metadata = json.loads(internal_metadata) From 753126b8ccb56dc6539ce95758f3e87fe181064d Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Tue, 6 Jan 2015 11:18:12 +0000 Subject: [PATCH 08/36] Add some debug logging --- synapse/storage/state.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/synapse/storage/state.py b/synapse/storage/state.py index ab8090971..9aeb0b406 100644 --- a/synapse/storage/state.py +++ b/synapse/storage/state.py @@ -15,6 +15,10 @@ from ._base import SQLBaseStore +import logging + +logger = logging.getLogger(__name__) + class StateStore(SQLBaseStore): """ Keeps track of the state at a given event. @@ -54,6 +58,8 @@ class StateStore(SQLBaseStore): if group: groups.add(group) + logger.debug("Got groups: %s", groups) + res = {} for group in groups: state_ids = self._simple_select_onecol_txn( @@ -62,6 +68,12 @@ class StateStore(SQLBaseStore): keyvalues={"state_group": group}, retcol="event_id", ) + + logger.debug( + "Got %d events for group %s", + len(state_ids), group + ) + state = self._get_events_txn(txn, state_ids) res[group] = state From f4ea78e9e2578f0d0ba4345b3b45390e905438e3 Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Tue, 6 Jan 2015 11:24:18 +0000 Subject: [PATCH 09/36] More debug logging --- synapse/storage/_base.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/synapse/storage/_base.py b/synapse/storage/_base.py index a30b0bc41..de08c78ed 100644 --- a/synapse/storage/_base.py +++ b/synapse/storage/_base.py @@ -443,6 +443,8 @@ class SQLBaseStore(object): if not event_ids: return [] + logger.debug("_get_events_txn called with %d events", len(event_ids)) + if len(event_ids) > 50: events = [] n = 50 @@ -451,6 +453,8 @@ class SQLBaseStore(object): events.extend(self._get_events_txn(txn, e_ids)) return events + logger.debug("_get_events_txn Fetching %d events", len(event_ids)) + where_clause = " OR ".join(["e.event_id = ?" for _ in event_ids]) sql = ( From 3e26720e0574393dc8076b3d4099e16213ce2e6d Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Tue, 6 Jan 2015 11:26:58 +0000 Subject: [PATCH 10/36] Temporarily turn off 'redacted_because' and 'prev_content' keys --- synapse/storage/_base.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/synapse/storage/_base.py b/synapse/storage/_base.py index de08c78ed..d63655643 100644 --- a/synapse/storage/_base.py +++ b/synapse/storage/_base.py @@ -499,6 +499,8 @@ class SQLBaseStore(object): ev = FrozenEvent(d, internal_metadata_dict=internal_metadata) + return ev + if check_redacted and redacted: ev = prune_event(ev) From 3c8c3bf3b781068637223d47d542d9d93a05a9b3 Mon Sep 17 00:00:00 2001 From: Mark Haines Date: Tue, 6 Jan 2015 11:32:36 +0000 Subject: [PATCH 11/36] SYN-229: Include Content-Length when downloading files --- synapse/media/v1/base_resource.py | 12 ++++++++++-- synapse/media/v1/download_resource.py | 12 +++++++++--- synapse/media/v1/thumbnail_resource.py | 6 ++++-- 3 files changed, 23 insertions(+), 7 deletions(-) diff --git a/synapse/media/v1/base_resource.py b/synapse/media/v1/base_resource.py index 2f5440ab6..38e01970c 100644 --- a/synapse/media/v1/base_resource.py +++ b/synapse/media/v1/base_resource.py @@ -201,7 +201,8 @@ class BaseMediaResource(Resource): defer.returnValue(media_info) @defer.inlineCallbacks - def _respond_with_file(self, request, media_type, file_path): + def _respond_with_file(self, request, media_type, file_path, + file_size=None): logger.debug("Responding with %r", file_path) if os.path.isfile(file_path): @@ -215,13 +216,20 @@ class BaseMediaResource(Resource): request.setHeader( b"Cache-Control", b"public,max-age=86400,s-maxage=86400" ) + if file_size is None: + stat = os.stat(file_path) + file_size = stat.st_size + + request.setHeader( + b"Content-Length", b"%d" % (file_size,) + ) with open(file_path, "rb") as f: yield FileSender().beginFileTransfer(f, request) request.finish() else: - self._respond_404() + self._respond_404(request) def _get_thumbnail_requirements(self, media_type): if media_type == "image/jpeg": diff --git a/synapse/media/v1/download_resource.py b/synapse/media/v1/download_resource.py index f3a6804e0..8b5072ebb 100644 --- a/synapse/media/v1/download_resource.py +++ b/synapse/media/v1/download_resource.py @@ -46,23 +46,29 @@ class DownloadResource(BaseMediaResource): def _respond_local_file(self, request, media_id): media_info = yield self.store.get_local_media(media_id) if not media_info: - self._respond_404() + self._respond_404(request) return media_type = media_info["media_type"] + media_length = media_info["media_length"] file_path = self.filepaths.local_media_filepath(media_id) - yield self._respond_with_file(request, media_type, file_path) + yield self._respond_with_file( + request, media_type, file_path, media_length + ) @defer.inlineCallbacks def _respond_remote_file(self, request, server_name, media_id): media_info = yield self._get_remote_media(server_name, media_id) media_type = media_info["media_type"] + media_length = media_info["media_length"] filesystem_id = media_info["filesystem_id"] file_path = self.filepaths.remote_media_filepath( server_name, filesystem_id ) - yield self._respond_with_file(request, media_type, file_path) + yield self._respond_with_file( + request, media_type, file_path, media_length + ) diff --git a/synapse/media/v1/thumbnail_resource.py b/synapse/media/v1/thumbnail_resource.py index 5ddcf54b9..666764203 100644 --- a/synapse/media/v1/thumbnail_resource.py +++ b/synapse/media/v1/thumbnail_resource.py @@ -100,11 +100,12 @@ class ThumbnailResource(BaseMediaResource): t_type = thumbnail_info["thumbnail_type"] t_method = thumbnail_info["thumbnail_method"] file_id = thumbnail_info["filesystem_id"] + t_length = thumbnail_info["thumbnail_length"] file_path = self.filepaths.remote_media_thumbnail( server_name, file_id, t_width, t_height, t_type, t_method, ) - yield self._respond_with_file(request, t_type, file_path) + yield self._respond_with_file(request, t_type, file_path, t_length) else: yield self._respond_default_thumbnail( request, media_info, width, height, method, m_type, @@ -139,11 +140,12 @@ class ThumbnailResource(BaseMediaResource): t_height = thumbnail_info["thumbnail_height"] t_type = thumbnail_info["thumbnail_type"] t_method = thumbnail_info["thumbnail_method"] + t_length = thumbnail_info["thumbnail_length"] file_path = self.filepaths.default_thumbnail( top_level_type, sub_type, t_width, t_height, t_type, t_method, ) - yield self.respond_with_file(request, t_type, file_path) + yield self.respond_with_file(request, t_type, file_path, t_length) def _select_thumbnail(self, desired_width, desired_height, desired_method, desired_type, thumbnail_infos): From 78edb47cc53e52504f2ceb8efa23ae1e50b66946 Mon Sep 17 00:00:00 2001 From: Kegan Dougal Date: Tue, 6 Jan 2015 11:43:04 +0000 Subject: [PATCH 12/36] SYN-208/SYN-228: Add runtime checks on startup to enforce that JPEG/PNG support is included when installing pillow. --- synapse/media/v1/__init__.py | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/synapse/media/v1/__init__.py b/synapse/media/v1/__init__.py index e69de29bb..2b1762dcd 100644 --- a/synapse/media/v1/__init__.py +++ b/synapse/media/v1/__init__.py @@ -0,0 +1,29 @@ +# -*- coding: utf-8 -*- +import PIL.Image + +# check for JPEG support. +try: + PIL.Image._getdecoder("rgb", "jpeg", None) +except IOError as e: + if str(e).startswith("decoder jpeg not available"): + raise Exception( + "FATAL: jpeg codec not supported. Install pillow correctly! " + " 'sudo apt-get install libjpeg-dev' then 'pip install -I pillow'" + ) +except Exception: + # any other exception is fine + pass + + +# check for PNG support. +try: + PIL.Image._getdecoder("rgb", "zip", None) +except IOError as e: + if str(e).startswith("decoder zip not available"): + raise Exception( + "FATAL: zip codec not supported. Install pillow correctly! " + " 'sudo apt-get install libjpeg-dev' then 'pip install -I pillow'" + ) +except Exception: + # any other exception is fine + pass \ No newline at end of file From 98933e3db6d43dcb3c8c21d0b65e2647bc3fb303 Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Tue, 6 Jan 2015 13:03:23 +0000 Subject: [PATCH 13/36] Only fetch prev_content when a client is streaming/paginating. Use transactions for event streams. --- synapse/storage/_base.py | 32 ++++++--- synapse/storage/stream.py | 142 ++++++++++++++++++-------------------- 2 files changed, 90 insertions(+), 84 deletions(-) diff --git a/synapse/storage/_base.py b/synapse/storage/_base.py index d63655643..9702ab4f4 100644 --- a/synapse/storage/_base.py +++ b/synapse/storage/_base.py @@ -434,12 +434,15 @@ class SQLBaseStore(object): return self.runInteraction("_simple_max_id", func) - def _get_events(self, event_ids): + def _get_events(self, event_ids, check_redacted=True, + get_prev_content=False): return self.runInteraction( - "_get_events", self._get_events_txn, event_ids + "_get_events", self._get_events_txn, event_ids, + check_redacted=check_redacted, get_prev_content=get_prev_content, ) - def _get_events_txn(self, txn, event_ids): + def _get_events_txn(self, txn, event_ids, check_redacted=True, + get_prev_content=False): if not event_ids: return [] @@ -450,7 +453,13 @@ class SQLBaseStore(object): n = 50 split = [event_ids[i:i + n] for i in range(0, len(event_ids), n)] for e_ids in split: - events.extend(self._get_events_txn(txn, e_ids)) + events.extend( + self._get_events_txn( + txn, e_ids, + check_redacted=check_redacted, + get_prev_content=get_prev_content, + ) + ) return events logger.debug("_get_events_txn Fetching %d events", len(event_ids)) @@ -467,10 +476,17 @@ class SQLBaseStore(object): res = txn.fetchall() - return [self._get_event_from_row_txn(txn, *r) for r in res] + return [ + self._get_event_from_row_txn( + txn, r[0], r[1], r[2], + check_redacted=check_redacted, + get_prev_content=get_prev_content, + ) + for r in res + ] def _get_event_txn(self, txn, event_id, check_redacted=True, - get_prev_content=True): + get_prev_content=False): sql = ( "SELECT internal_metadata, json, r.event_id FROM event_json as e " "LEFT JOIN redactions as r ON e.event_id = r.redacts " @@ -493,14 +509,12 @@ class SQLBaseStore(object): ) def _get_event_from_row_txn(self, txn, internal_metadata, js, redacted, - check_redacted=True, get_prev_content=True): + check_redacted=True, get_prev_content=False): d = json.loads(js) internal_metadata = json.loads(internal_metadata) ev = FrozenEvent(d, internal_metadata_dict=internal_metadata) - return ev - if check_redacted and redacted: ev = prune_event(ev) diff --git a/synapse/storage/stream.py b/synapse/storage/stream.py index 3405cb365..c51f48945 100644 --- a/synapse/storage/stream.py +++ b/synapse/storage/stream.py @@ -137,12 +137,12 @@ class StreamStore(SQLBaseStore): with_feedback=with_feedback, ) - @defer.inlineCallbacks @log_function def get_room_events_stream(self, user_id, from_key, to_key, room_id, limit=0, with_feedback=False): # TODO (erikj): Handle compressed feedback + current_room_membership_sql = ( "SELECT m.room_id FROM room_memberships as m " "INNER JOIN current_state_events as c ON m.event_id = c.event_id " @@ -157,11 +157,6 @@ class StreamStore(SQLBaseStore): "WHERE m.user_id = ? " ) - del_sql = ( - "SELECT event_id FROM redactions WHERE redacts = e.event_id " - "LIMIT 1" - ) - if limit: limit = max(limit, MAX_STREAM_SIZE) else: @@ -172,38 +167,42 @@ class StreamStore(SQLBaseStore): to_id = _parse_stream_token(to_key) if from_key == to_key: - defer.returnValue(([], to_key)) - return + return defer.succeed(([], to_key)) sql = ( - "SELECT *, (%(redacted)s) AS redacted FROM events AS e WHERE " + "SELECT e.event_id, e.stream_ordering FROM events AS e WHERE " "(e.outlier = 0 AND (room_id IN (%(current)s)) OR " "(event_id IN (%(invites)s))) " "AND e.stream_ordering > ? AND e.stream_ordering <= ? " "ORDER BY stream_ordering ASC LIMIT %(limit)d " ) % { - "redacted": del_sql, "current": current_room_membership_sql, "invites": membership_sql, "limit": limit } - rows = yield self._execute_and_decode( - sql, - user_id, user_id, from_id, to_id - ) + def f(txn): + txn.execute(sql, (user_id, user_id, from_id, to_id,)) - ret = yield self._parse_events(rows) + rows = self.cursor_to_dict(txn) - if rows: - key = "s%d" % max([r["stream_ordering"] for r in rows]) - else: - # Assume we didn't get anything because there was nothing to get. - key = to_key + ret = self._get_events_txn( + txn, + [r["event_id"] for r in rows], + get_prev_content=True + ) - defer.returnValue((ret, key)) + if rows: + key = "s%d" % max([r["stream_ordering"] for r in rows]) + else: + # Assume we didn't get anything because there was nothing to + # get. + key = to_key + + return ret, key + + return self.runInteraction("get_room_events_stream", f) - @defer.inlineCallbacks @log_function def paginate_room_events(self, room_id, from_key, to_key=None, direction='b', limit=-1, @@ -221,7 +220,9 @@ class StreamStore(SQLBaseStore): bounds = _get_token_bound(from_key, from_comp) if to_key: - bounds = "%s AND %s" % (bounds, _get_token_bound(to_key, to_comp)) + bounds = "%s AND %s" % ( + bounds, _get_token_bound(to_key, to_comp) + ) if int(limit) > 0: args.append(int(limit)) @@ -229,87 +230,78 @@ class StreamStore(SQLBaseStore): else: limit_str = "" - del_sql = ( - "SELECT event_id FROM redactions WHERE redacts = events.event_id " - "LIMIT 1" - ) - sql = ( - "SELECT *, (%(redacted)s) AS redacted FROM events" + "SELECT * FROM events" " WHERE outlier = 0 AND room_id = ? AND %(bounds)s" " ORDER BY topological_ordering %(order)s," " stream_ordering %(order)s %(limit)s" ) % { - "redacted": del_sql, "bounds": bounds, "order": order, "limit": limit_str } - rows = yield self._execute_and_decode( - sql, - *args - ) + def f(txn): + txn.execute(sql, args) - if rows: - topo = rows[-1]["topological_ordering"] - toke = rows[-1]["stream_ordering"] - if direction == 'b': - topo -= 1 - toke -= 1 - next_token = "t%s-%s" % (topo, toke) - else: - # TODO (erikj): We should work out what to do here instead. - next_token = to_key if to_key else from_key + rows = self.cursor_to_dict(txn) - events = yield self._parse_events(rows) + if rows: + topo = rows[-1]["topological_ordering"] + toke = rows[-1]["stream_ordering"] + if direction == 'b': + topo -= 1 + toke -= 1 + next_token = "t%s-%s" % (topo, toke) + else: + # TODO (erikj): We should work out what to do here instead. + next_token = to_key if to_key else from_key - defer.returnValue( - ( - events, - next_token + events = self._get_events_txn( + txn, + [r["event_id"] for r in rows], + get_prev_content=True ) - ) - @defer.inlineCallbacks + return events, next_token, + + return self.runInteraction("paginate_room_events", f) + def get_recent_events_for_room(self, room_id, limit, end_token, with_feedback=False): # TODO (erikj): Handle compressed feedback - del_sql = ( - "SELECT event_id FROM redactions WHERE redacts = events.event_id " - "LIMIT 1" - ) - sql = ( - "SELECT *, (%(redacted)s) AS redacted FROM events " + "SELECT * FROM events " "WHERE room_id = ? AND stream_ordering <= ? AND outlier = 0 " "ORDER BY topological_ordering DESC, stream_ordering DESC LIMIT ? " - ) % { - "redacted": del_sql, - } - - rows = yield self._execute_and_decode( - sql, - room_id, end_token, limit ) - rows.reverse() # As we selected with reverse ordering + def f(txn): + txn.execute(sql, (room_id, end_token, limit,)) - if rows: - topo = rows[0]["topological_ordering"] - toke = rows[0]["stream_ordering"] - start_token = "t%s-%s" % (topo, toke) + rows = self.cursor_to_dict(txn) - token = (start_token, end_token) - else: - token = (end_token, end_token) + rows.reverse() # As we selected with reverse ordering - events = yield self._parse_events(rows) + if rows: + topo = rows[0]["topological_ordering"] + toke = rows[0]["stream_ordering"] + start_token = "t%s-%s" % (topo, toke) - ret = (events, token) + token = (start_token, end_token) + else: + token = (end_token, end_token) - defer.returnValue(ret) + events = self._get_events_txn( + txn, + [r["event_id"] for r in rows], + get_prev_content=True + ) + + return events, token + + return self.runInteraction("get_recent_events_for_room", f) def get_room_events_max_id(self): return self.runInteraction( From 773de09774399692cb43d56587ae77cbbee42d01 Mon Sep 17 00:00:00 2001 From: Mark Haines Date: Tue, 6 Jan 2015 13:05:07 +0000 Subject: [PATCH 14/36] Set a content-length for JSON responses --- synapse/http/server.py | 1 + 1 file changed, 1 insertion(+) diff --git a/synapse/http/server.py b/synapse/http/server.py index 5765dffe3..de26afb48 100644 --- a/synapse/http/server.py +++ b/synapse/http/server.py @@ -233,6 +233,7 @@ def respond_with_json_bytes(request, code, json_bytes, send_cors=False, request.setResponseCode(code, message=response_code_message) request.setHeader(b"Content-Type", b"application/json") request.setHeader(b"Server", AGENT_NAME) + request.setHeader(b"Content-Length", b"%d" % len(json_bytes)) if send_cors: request.setHeader("Access-Control-Allow-Origin", "*") From 52d85190081044b9fbaf24869d652d3fe3c23e5d Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Tue, 6 Jan 2015 13:10:27 +0000 Subject: [PATCH 15/36] Don't do batching when getting events. --- synapse/storage/_base.py | 39 ++++----------------------------------- 1 file changed, 4 insertions(+), 35 deletions(-) diff --git a/synapse/storage/_base.py b/synapse/storage/_base.py index 9702ab4f4..9687222e7 100644 --- a/synapse/storage/_base.py +++ b/synapse/storage/_base.py @@ -446,43 +446,12 @@ class SQLBaseStore(object): if not event_ids: return [] - logger.debug("_get_events_txn called with %d events", len(event_ids)) - - if len(event_ids) > 50: - events = [] - n = 50 - split = [event_ids[i:i + n] for i in range(0, len(event_ids), n)] - for e_ids in split: - events.extend( - self._get_events_txn( - txn, e_ids, - check_redacted=check_redacted, - get_prev_content=get_prev_content, - ) - ) - return events - - logger.debug("_get_events_txn Fetching %d events", len(event_ids)) - - where_clause = " OR ".join(["e.event_id = ?" for _ in event_ids]) - - sql = ( - "SELECT internal_metadata, json, r.event_id FROM event_json as e " - "LEFT JOIN redactions as r ON e.event_id = r.redacts " - "WHERE %s" - ) % (where_clause,) - - txn.execute(sql, event_ids) - - res = txn.fetchall() - return [ - self._get_event_from_row_txn( - txn, r[0], r[1], r[2], - check_redacted=check_redacted, - get_prev_content=get_prev_content, + self._get_event_txn( + txn, event_id, + check_redacted=check_redacted, get_prev_content=get_prev_content ) - for r in res + for event_id in event_ids ] def _get_event_txn(self, txn, event_id, check_redacted=True, From 12819d5082ac73adc309428770c9270ba378c6e2 Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Tue, 6 Jan 2015 13:12:30 +0000 Subject: [PATCH 16/36] Remove debug lines --- synapse/storage/state.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/synapse/storage/state.py b/synapse/storage/state.py index 9aeb0b406..fd6f1e3b0 100644 --- a/synapse/storage/state.py +++ b/synapse/storage/state.py @@ -58,8 +58,6 @@ class StateStore(SQLBaseStore): if group: groups.add(group) - logger.debug("Got groups: %s", groups) - res = {} for group in groups: state_ids = self._simple_select_onecol_txn( @@ -69,11 +67,6 @@ class StateStore(SQLBaseStore): retcol="event_id", ) - logger.debug( - "Got %d events for group %s", - len(state_ids), group - ) - state = self._get_events_txn(txn, state_ids) res[group] = state From af1c7c7808d297711e4b76b862c41a5ec2ca3a9a Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Tue, 6 Jan 2015 13:13:17 +0000 Subject: [PATCH 17/36] PEP8 --- synapse/storage/_base.py | 6 ++++-- synapse/storage/stream.py | 1 - 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/synapse/storage/_base.py b/synapse/storage/_base.py index 9687222e7..e799ac6c5 100644 --- a/synapse/storage/_base.py +++ b/synapse/storage/_base.py @@ -449,7 +449,8 @@ class SQLBaseStore(object): return [ self._get_event_txn( txn, event_id, - check_redacted=check_redacted, get_prev_content=get_prev_content + check_redacted=check_redacted, + get_prev_content=get_prev_content ) for event_id in event_ids ] @@ -473,7 +474,8 @@ class SQLBaseStore(object): internal_metadata, js, redacted = res return self._get_event_from_row_txn( - txn, internal_metadata, js, redacted, check_redacted=check_redacted, + txn, internal_metadata, js, redacted, + check_redacted=check_redacted, get_prev_content=get_prev_content, ) diff --git a/synapse/storage/stream.py b/synapse/storage/stream.py index c51f48945..bd3a411ea 100644 --- a/synapse/storage/stream.py +++ b/synapse/storage/stream.py @@ -142,7 +142,6 @@ class StreamStore(SQLBaseStore): limit=0, with_feedback=False): # TODO (erikj): Handle compressed feedback - current_room_membership_sql = ( "SELECT m.room_id FROM room_memberships as m " "INNER JOIN current_state_events as c ON m.event_id = c.event_id " From adb04b1e572d13b75541f4684aac3683e94d70b8 Mon Sep 17 00:00:00 2001 From: Mark Haines Date: Tue, 6 Jan 2015 13:21:39 +0000 Subject: [PATCH 18/36] Update copyright notices --- scripts/copyrighter.pl | 2 +- synapse/__init__.py | 2 +- synapse/api/__init__.py | 2 +- synapse/api/auth.py | 2 +- synapse/api/constants.py | 2 +- synapse/api/errors.py | 2 +- synapse/api/ratelimiting.py | 2 +- synapse/api/urls.py | 2 +- synapse/app/__init__.py | 2 +- synapse/app/homeserver.py | 2 +- synapse/app/synctl.py | 2 +- synapse/config/__init__.py | 2 +- synapse/config/_base.py | 2 +- synapse/config/captcha.py | 2 +- synapse/config/database.py | 2 +- synapse/config/email.py | 2 +- synapse/config/homeserver.py | 2 +- synapse/config/logger.py | 2 +- synapse/config/ratelimiting.py | 2 +- synapse/config/repository.py | 2 +- synapse/config/server.py | 2 +- synapse/config/tls.py | 2 +- synapse/config/voip.py | 2 +- synapse/crypto/__init__.py | 2 +- synapse/crypto/context_factory.py | 2 +- synapse/crypto/event_signing.py | 2 +- synapse/crypto/keyclient.py | 2 +- synapse/crypto/keyring.py | 2 +- synapse/events/__init__.py | 2 +- synapse/events/builder.py | 2 +- synapse/events/snapshot.py | 2 +- synapse/events/utils.py | 2 +- synapse/events/validator.py | 2 +- synapse/federation/__init__.py | 2 +- synapse/federation/persistence.py | 2 +- synapse/federation/replication.py | 2 +- synapse/federation/transport.py | 2 +- synapse/federation/units.py | 2 +- synapse/handlers/__init__.py | 2 +- synapse/handlers/_base.py | 2 +- synapse/handlers/admin.py | 2 +- synapse/handlers/directory.py | 2 +- synapse/handlers/events.py | 2 +- synapse/handlers/federation.py | 2 +- synapse/handlers/login.py | 2 +- synapse/handlers/message.py | 2 +- synapse/handlers/presence.py | 2 +- synapse/handlers/profile.py | 2 +- synapse/handlers/register.py | 2 +- synapse/handlers/room.py | 2 +- synapse/handlers/typing.py | 2 +- synapse/http/__init__.py | 2 +- synapse/http/agent_name.py | 2 +- synapse/http/client.py | 2 +- synapse/http/endpoint.py | 2 +- synapse/http/matrixfederationclient.py | 2 +- synapse/http/server.py | 4 ++-- synapse/http/server_key_resource.py | 2 +- synapse/media/v0/content_repository.py | 2 +- synapse/media/v1/__init__.py | 16 +++++++++++++++- synapse/media/v1/base_resource.py | 2 +- synapse/media/v1/download_resource.py | 2 +- synapse/media/v1/filepath.py | 2 +- synapse/media/v1/media_repository.py | 2 +- synapse/media/v1/thumbnail_resource.py | 2 +- synapse/media/v1/thumbnailer.py | 2 +- synapse/media/v1/upload_resource.py | 2 +- synapse/notifier.py | 2 +- synapse/rest/__init__.py | 2 +- synapse/rest/admin.py | 2 +- synapse/rest/base.py | 2 +- synapse/rest/directory.py | 2 +- synapse/rest/events.py | 2 +- synapse/rest/initial_sync.py | 2 +- synapse/rest/login.py | 2 +- synapse/rest/presence.py | 2 +- synapse/rest/profile.py | 2 +- synapse/rest/register.py | 2 +- synapse/rest/room.py | 2 +- synapse/rest/transactions.py | 2 +- synapse/rest/voip.py | 2 +- synapse/server.py | 2 +- synapse/state.py | 2 +- synapse/storage/__init__.py | 2 +- synapse/storage/_base.py | 2 +- synapse/storage/directory.py | 2 +- synapse/storage/event_federation.py | 2 +- synapse/storage/feedback.py | 2 +- synapse/storage/keys.py | 2 +- synapse/storage/media_repository.py | 2 +- synapse/storage/presence.py | 2 +- synapse/storage/profile.py | 2 +- synapse/storage/registration.py | 2 +- synapse/storage/room.py | 2 +- synapse/storage/roommember.py | 2 +- synapse/storage/schema/delta/v2.sql | 2 +- synapse/storage/schema/delta/v3.sql | 2 +- synapse/storage/schema/delta/v4.sql | 14 ++++++++++++++ synapse/storage/schema/delta/v5.sql | 14 ++++++++++++++ synapse/storage/schema/delta/v6.sql | 2 +- synapse/storage/schema/delta/v8.sql | 2 +- synapse/storage/schema/delta/v9.sql | 2 +- synapse/storage/schema/event_edges.sql | 14 ++++++++++++++ synapse/storage/schema/event_signatures.sql | 2 +- synapse/storage/schema/im.sql | 2 +- synapse/storage/schema/keys.sql | 2 +- synapse/storage/schema/media_repository.sql | 2 +- synapse/storage/schema/presence.sql | 2 +- synapse/storage/schema/profiles.sql | 2 +- synapse/storage/schema/redactions.sql | 14 ++++++++++++++ synapse/storage/schema/room_aliases.sql | 2 +- synapse/storage/schema/state.sql | 2 +- synapse/storage/schema/transactions.sql | 2 +- synapse/storage/schema/users.sql | 2 +- synapse/storage/signatures.py | 2 +- synapse/storage/state.py | 2 +- synapse/storage/stream.py | 2 +- synapse/storage/transactions.py | 2 +- synapse/streams/__init__.py | 2 +- synapse/streams/config.py | 2 +- synapse/streams/events.py | 2 +- synapse/types.py | 2 +- synapse/util/__init__.py | 2 +- synapse/util/async.py | 2 +- synapse/util/distributor.py | 2 +- synapse/util/emailutils.py | 2 +- synapse/util/frozenutils.py | 2 +- synapse/util/jsonobject.py | 2 +- synapse/util/lockutils.py | 2 +- synapse/util/logcontext.py | 14 ++++++++++++++ synapse/util/logutils.py | 2 +- synapse/util/stringutils.py | 2 +- 132 files changed, 212 insertions(+), 128 deletions(-) diff --git a/scripts/copyrighter.pl b/scripts/copyrighter.pl index 7c03ef21f..a913d74c8 100755 --- a/scripts/copyrighter.pl +++ b/scripts/copyrighter.pl @@ -14,7 +14,7 @@ # limitations under the License. $copyright = < Date: Tue, 6 Jan 2015 14:04:43 +0000 Subject: [PATCH 19/36] Increase default maximum attachment size to 10M --- synapse/config/repository.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/synapse/config/repository.py b/synapse/config/repository.py index 017374919..e1827f05e 100644 --- a/synapse/config/repository.py +++ b/synapse/config/repository.py @@ -37,7 +37,7 @@ class ContentRepositoryConfig(Config): super(ContentRepositoryConfig, cls).add_arguments(parser) db_group = parser.add_argument_group("content_repository") db_group.add_argument( - "--max-upload-size", default="1M" + "--max-upload-size", default="10M" ) db_group.add_argument( "--media-store-path", default=cls.default_path("media_store") From 0529a7e2e9284f479f50160b48b4e4686076c06a Mon Sep 17 00:00:00 2001 From: Matrix Date: Tue, 6 Jan 2015 14:06:25 +0000 Subject: [PATCH 20/36] Add some logging for when we are sending transactions. --- synapse/federation/replication.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/synapse/federation/replication.py b/synapse/federation/replication.py index 2aa1149d3..a4c29b484 100644 --- a/synapse/federation/replication.py +++ b/synapse/federation/replication.py @@ -730,6 +730,7 @@ class _TransactionQueue(object): destinations = set(destinations) destinations.discard(self.server_name) + destinations.discard("localhost") logger.debug("Sending to: %s", str(destinations)) @@ -814,6 +815,8 @@ class _TransactionQueue(object): else: logger.info("TX [%s] is ready for retry", destination) + logger.info("TX [%s] _attempt_new_transaction", destination) + if destination in self.pending_transactions: # XXX: pending_transactions can get stuck on by a never-ending # request at which point pending_pdus_by_dest just keeps growing. @@ -826,6 +829,9 @@ class _TransactionQueue(object): pending_edus = self.pending_edus_by_dest.pop(destination, []) pending_failures = self.pending_failures_by_dest.pop(destination, []) + if pending_pdus: + logger.info("TX [%s] len(pending_pdus_by_dest[dest]) = %d", destination, len(pending_pdus)) + if not pending_pdus and not pending_edus and not pending_failures: return From bc2ec808f431b41e8d39a155a9fd0b5177bccb45 Mon Sep 17 00:00:00 2001 From: Mark Haines Date: Tue, 6 Jan 2015 14:13:18 +0000 Subject: [PATCH 21/36] SYN-32 Use the ANTIALIAS resize method for thumbnailing images --- synapse/media/v1/thumbnailer.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/synapse/media/v1/thumbnailer.py b/synapse/media/v1/thumbnailer.py index 65468cf60..bc86efea8 100644 --- a/synapse/media/v1/thumbnailer.py +++ b/synapse/media/v1/thumbnailer.py @@ -48,7 +48,7 @@ class Thumbnailer(object): def scale(self, output_path, width, height, output_type): """Rescales the image to the given dimensions""" - scaled = self.image.resize((width, height), Image.BILINEAR) + scaled = self.image.resize((width, height), Image.ANTIALIAS) return self.save_image(scaled, output_type, output_path) def crop(self, output_path, width, height, output_type): @@ -65,7 +65,7 @@ class Thumbnailer(object): if width * self.height > height * self.width: scaled_height = (width * self.height) // self.width scaled_image = self.image.resize( - (width, scaled_height), Image.BILINEAR + (width, scaled_height), Image.ANTIALIAS ) crop_top = (scaled_height - height) // 2 crop_bottom = height + crop_top @@ -73,7 +73,7 @@ class Thumbnailer(object): else: scaled_width = (height * self.width) // self.height scaled_image = self.image.resize( - (scaled_width, height), Image.BILINEAR + (scaled_width, height), Image.ANTIALIAS ) crop_left = (scaled_width - width) // 2 crop_right = width + crop_left From 76ec154e95518fe91d430c76303feadf6c76dabc Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Tue, 6 Jan 2015 14:37:00 +0000 Subject: [PATCH 22/36] We don't need the full events for get_rooms_for_user_where_membership_is --- synapse/handlers/profile.py | 10 +++++----- synapse/storage/roommember.py | 36 ++++++++++++++++++++++++++++++----- 2 files changed, 36 insertions(+), 10 deletions(-) diff --git a/synapse/handlers/profile.py b/synapse/handlers/profile.py index 3f11e2dcf..8d4d44150 100644 --- a/synapse/handlers/profile.py +++ b/synapse/handlers/profile.py @@ -16,7 +16,7 @@ from twisted.internet import defer from synapse.api.errors import SynapseError, AuthError, CodeMessageException -from synapse.api.constants import Membership +from synapse.api.constants import EventTypes, Membership from synapse.util.logcontext import PreserveLoggingContext from ._base import BaseHandler @@ -203,7 +203,7 @@ class ProfileHandler(BaseHandler): for j in joins: content = { - "membership": j.content["membership"], + "membership": Membership.JOIN, } yield self.distributor.fire( @@ -212,9 +212,9 @@ class ProfileHandler(BaseHandler): msg_handler = self.hs.get_handlers().message_handler yield msg_handler.create_and_send_event({ - "type": j.type, + "type": EventTypes.Member, "room_id": j.room_id, - "state_key": j.state_key, + "state_key": user.to_string(), "content": content, - "sender": j.state_key + "sender": user.to_string() }, ratelimit=False) diff --git a/synapse/storage/roommember.py b/synapse/storage/roommember.py index 4921561fc..c495fab91 100644 --- a/synapse/storage/roommember.py +++ b/synapse/storage/roommember.py @@ -15,6 +15,8 @@ from twisted.internet import defer +from collections import namedtuple + from ._base import SQLBaseStore from synapse.api.constants import Membership @@ -24,6 +26,12 @@ import logging logger = logging.getLogger(__name__) +RoomsForUser = namedtuple( + "RoomsForUser", + ("room_id", "sender", "membership") +) + + class RoomMemberStore(SQLBaseStore): def _store_room_member_txn(self, txn, event): @@ -163,19 +171,37 @@ class RoomMemberStore(SQLBaseStore): membership_list (list): A list of synapse.api.constants.Membership values which the user must be in. Returns: - A list of RoomMemberEvent objects + A list of dictionary objects, with room_id, membership and sender + defined. """ if not membership_list: return defer.succeed(None) - args = [user_id] - args.extend(membership_list) - where_clause = "user_id = ? AND (%s)" % ( " OR ".join(["membership = ?" for _ in membership_list]), ) - return self._get_members_query(where_clause, args) + args = [user_id] + args.extend(membership_list) + + def f(txn): + sql = ( + "SELECT m.room_id, m.sender, m.membership" + " FROM room_memberships as m" + " INNER JOIN current_state_events as c" + " ON m.event_id = c.event_id" + " WHERE %s" + ) % (where_clause,) + + txn.execute(sql, args) + return [ + RoomsForUser(**r) for r in self.cursor_to_dict(txn) + ] + + return self.runInteraction( + "get_rooms_for_user_where_membership_is", + f + ) def get_joined_hosts_for_room(self, room_id): return self._simple_select_onecol( From 96707ed7185fc5c1668c8a31b62c67bdf39ed777 Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Tue, 6 Jan 2015 14:44:27 +0000 Subject: [PATCH 23/36] Name 'user_rooms_intersect' transaction --- synapse/storage/roommember.py | 34 ++++++++++++++++++---------------- 1 file changed, 18 insertions(+), 16 deletions(-) diff --git a/synapse/storage/roommember.py b/synapse/storage/roommember.py index c495fab91..11f8f7877 100644 --- a/synapse/storage/roommember.py +++ b/synapse/storage/roommember.py @@ -239,26 +239,28 @@ class RoomMemberStore(SQLBaseStore): results = self._parse_events_txn(txn, rows) return results - @defer.inlineCallbacks def user_rooms_intersect(self, user_id_list): """ Checks whether all the users whose IDs are given in a list share a room. """ - user_list_clause = " OR ".join(["m.user_id = ?"] * len(user_id_list)) - sql = ( - "SELECT m.room_id FROM room_memberships as m " - "INNER JOIN current_state_events as c " - "ON m.event_id = c.event_id " - "WHERE m.membership = 'join' " - "AND (%(clause)s) " - # TODO(paul): We've got duplicate rows in the database somewhere - # so we have to DISTINCT m.user_id here - "GROUP BY m.room_id HAVING COUNT(DISTINCT m.user_id) = ?" - ) % {"clause": user_list_clause} + def interaction(txn): + user_list_clause = " OR ".join(["m.user_id = ?"] * len(user_id_list)) + sql = ( + "SELECT m.room_id FROM room_memberships as m " + "INNER JOIN current_state_events as c " + "ON m.event_id = c.event_id " + "WHERE m.membership = 'join' " + "AND (%(clause)s) " + # TODO(paul): We've got duplicate rows in the database somewhere + # so we have to DISTINCT m.user_id here + "GROUP BY m.room_id HAVING COUNT(DISTINCT m.user_id) = ?" + ) % {"clause": user_list_clause} - args = list(user_id_list) - args.append(len(user_id_list)) + args = list(user_id_list) + args.append(len(user_id_list)) - rows = yield self._execute(None, sql, *args) + txn.execute(sql, args) - defer.returnValue(len(rows) > 0) + return len(txn.fetchall()) > 0 + + return self.runInteraction("user_rooms_intersect", interaction) From 52b2c6c9c73d47a269756d8da57b4dcff54e0d21 Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Tue, 6 Jan 2015 14:56:57 +0000 Subject: [PATCH 24/36] Don't include None's in _get_events_txn --- synapse/storage/_base.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/synapse/storage/_base.py b/synapse/storage/_base.py index 728d1df8f..a0b7f943f 100644 --- a/synapse/storage/_base.py +++ b/synapse/storage/_base.py @@ -446,7 +446,7 @@ class SQLBaseStore(object): if not event_ids: return [] - return [ + events = [ self._get_event_txn( txn, event_id, check_redacted=check_redacted, @@ -455,6 +455,8 @@ class SQLBaseStore(object): for event_id in event_ids ] + return [e for e in events if e] + def _get_event_txn(self, txn, event_id, check_redacted=True, get_prev_content=False): sql = ( From 03a501456ca8815a7d6fd8ea84d9c2a1feba33cf Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Tue, 6 Jan 2015 15:22:28 +0000 Subject: [PATCH 25/36] Time how long calls to _get_destination_retry_timings take --- synapse/storage/transactions.py | 3 ++ synapse/util/logutils.py | 73 +++++++++++++++++++++++++++------ 2 files changed, 64 insertions(+), 12 deletions(-) diff --git a/synapse/storage/transactions.py b/synapse/storage/transactions.py index e06ef3569..36ddf30d6 100644 --- a/synapse/storage/transactions.py +++ b/synapse/storage/transactions.py @@ -19,6 +19,8 @@ from collections import namedtuple from twisted.internet import defer +from synapse.util.logutils import time_function + import logging logger = logging.getLogger(__name__) @@ -228,6 +230,7 @@ class TransactionStore(SQLBaseStore): "get_destination_retry_timings", self._get_destination_retry_timings, destination) + @time_function def _get_destination_retry_timings(cls, txn, destination): query = DestinationsTable.select_statement("destination = ?") txn.execute(query, (destination,)) diff --git a/synapse/util/logutils.py b/synapse/util/logutils.py index 18ba405c4..c4dfb69c5 100644 --- a/synapse/util/logutils.py +++ b/synapse/util/logutils.py @@ -19,14 +19,37 @@ from functools import wraps import logging import inspect +import time + + +_TIME_FUNC_ID = 0 + + +def _log_debug_as_f(f, msg, msg_args): + name = f.__module__ + logger = logging.getLogger(name) + + if logger.isEnabledFor(logging.DEBUG): + lineno = f.func_code.co_firstlineno + pathname = f.func_code.co_filename + + record = logging.LogRecord( + name=name, + level=logging.DEBUG, + pathname=pathname, + lineno=lineno, + msg=msg, + args=msg_args, + exc_info=None + ) + + logger.handle(record) def log_function(f): """ Function decorator that logs every call to that function. """ func_name = f.__name__ - lineno = f.func_code.co_firstlineno - pathname = f.func_code.co_filename @wraps(f) def wrapped(*args, **kwargs): @@ -52,24 +75,50 @@ def log_function(f): "args": ", ".join(func_args) } - record = logging.LogRecord( - name=name, - level=level, - pathname=pathname, - lineno=lineno, - msg="Invoked '%(func_name)s' with args: %(args)s", - args=msg_args, - exc_info=None + _log_debug_as_f( + f, + "Invoked '%(func_name)s' with args: %(args)s", + msg_args ) - logger.handle(record) - return f(*args, **kwargs) wrapped.__name__ = func_name return wrapped +def time_function(f): + func_name = f.__name__ + + @wraps(f) + def wrapped(*args, **kwargs): + global _TIME_FUNC_ID + id = _TIME_FUNC_ID + _TIME_FUNC_ID += 1 + + start = time.clock() * 1000 + + try: + _log_debug_as_f( + f, + "[FUNC START] {%s-%d}", + (func_name, _TIME_FUNC_ID), + ) + + r = f(*args, **kwargs) + finally: + end = time.clock() * 1000 + _log_debug_as_f( + f, + "[FUNC END] {%s-%d} %f", + (func_name, _TIME_FUNC_ID, end-start,), + ) + + return r + + return wrapped + + def trace_function(f): func_name = f.__name__ linenum = f.func_code.co_firstlineno From 9bd07bed238337151ae79dc948f49cdf7141578c Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Tue, 6 Jan 2015 15:28:56 +0000 Subject: [PATCH 26/36] Actually time that function --- synapse/storage/transactions.py | 1 + 1 file changed, 1 insertion(+) diff --git a/synapse/storage/transactions.py b/synapse/storage/transactions.py index 36ddf30d6..9d14f8930 100644 --- a/synapse/storage/transactions.py +++ b/synapse/storage/transactions.py @@ -200,6 +200,7 @@ class TransactionStore(SQLBaseStore): self._get_transactions_after, transaction_id, destination ) + @time_function def _get_transactions_after(cls, txn, transaction_id, destination): where = ( "destination = ? AND id > (select id FROM %s WHERE " From f6da237c353d598946a6c81260653203602800c2 Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Tue, 6 Jan 2015 15:40:38 +0000 Subject: [PATCH 27/36] Add index on transaction_id to sent_transcations --- synapse/storage/schema/transactions.sql | 1 + synapse/storage/transactions.py | 4 ---- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/synapse/storage/schema/transactions.sql b/synapse/storage/schema/transactions.sql index 86f530d82..2d30f99b0 100644 --- a/synapse/storage/schema/transactions.sql +++ b/synapse/storage/schema/transactions.sql @@ -42,6 +42,7 @@ CREATE INDEX IF NOT EXISTS sent_transaction_dest ON sent_transactions(destinatio CREATE INDEX IF NOT EXISTS sent_transaction_dest_referenced ON sent_transactions( destination ); +CREATE INDEX IF NOT EXISTS sent_transaction_txn_id ON sent_transactions(transaction_id); -- So that we can do an efficient look up of all transactions that have yet to be successfully -- sent. CREATE INDEX IF NOT EXISTS sent_transaction_sent ON sent_transactions(response_code); diff --git a/synapse/storage/transactions.py b/synapse/storage/transactions.py index 9d14f8930..e06ef3569 100644 --- a/synapse/storage/transactions.py +++ b/synapse/storage/transactions.py @@ -19,8 +19,6 @@ from collections import namedtuple from twisted.internet import defer -from synapse.util.logutils import time_function - import logging logger = logging.getLogger(__name__) @@ -200,7 +198,6 @@ class TransactionStore(SQLBaseStore): self._get_transactions_after, transaction_id, destination ) - @time_function def _get_transactions_after(cls, txn, transaction_id, destination): where = ( "destination = ? AND id > (select id FROM %s WHERE " @@ -231,7 +228,6 @@ class TransactionStore(SQLBaseStore): "get_destination_retry_timings", self._get_destination_retry_timings, destination) - @time_function def _get_destination_retry_timings(cls, txn, destination): query = DestinationsTable.select_statement("destination = ?") txn.execute(query, (destination,)) From a01416cf217db5e442ff90715cbe1955b67b1efb Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Tue, 6 Jan 2015 15:42:18 +0000 Subject: [PATCH 28/36] Add delta and bump DB version --- synapse/storage/__init__.py | 2 +- synapse/storage/schema/delta/v11.sql | 16 ++++++++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) create mode 100644 synapse/storage/schema/delta/v11.sql diff --git a/synapse/storage/__init__.py b/synapse/storage/__init__.py index 8d482dece..4beb951b9 100644 --- a/synapse/storage/__init__.py +++ b/synapse/storage/__init__.py @@ -66,7 +66,7 @@ SCHEMAS = [ # Remember to update this number every time an incompatible change is made to # database schema files, so the users will be informed on server restarts. -SCHEMA_VERSION = 10 +SCHEMA_VERSION = 11 class _RollbackButIsFineException(Exception): diff --git a/synapse/storage/schema/delta/v11.sql b/synapse/storage/schema/delta/v11.sql new file mode 100644 index 000000000..313592221 --- /dev/null +++ b/synapse/storage/schema/delta/v11.sql @@ -0,0 +1,16 @@ +/* Copyright 2015 OpenMarket Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +CREATE INDEX IF NOT EXISTS sent_transaction_txn_id ON sent_transactions(transaction_id); \ No newline at end of file From 9e5545a6fac24e15a0493473548a669df85052b1 Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Tue, 6 Jan 2015 15:53:50 +0000 Subject: [PATCH 29/36] RoomsForUser now has sender instead of user_id --- synapse/handlers/message.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/synapse/handlers/message.py b/synapse/handlers/message.py index 24f8c1bc8..7195de98b 100644 --- a/synapse/handlers/message.py +++ b/synapse/handlers/message.py @@ -263,7 +263,7 @@ class MessageHandler(BaseHandler): } if event.membership == Membership.INVITE: - d["inviter"] = event.user_id + d["inviter"] = event.sender rooms_ret.append(d) From fd9a8db7ea93225af1774a604a97e84315cdccc8 Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Tue, 6 Jan 2015 15:59:31 +0000 Subject: [PATCH 30/36] Only fetch the columns we need. --- synapse/storage/stream.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/synapse/storage/stream.py b/synapse/storage/stream.py index a5e1c38f7..bedc3c6c5 100644 --- a/synapse/storage/stream.py +++ b/synapse/storage/stream.py @@ -271,7 +271,7 @@ class StreamStore(SQLBaseStore): # TODO (erikj): Handle compressed feedback sql = ( - "SELECT * FROM events " + "SELECT stream_ordering, topological_ordering, event_id FROM events " "WHERE room_id = ? AND stream_ordering <= ? AND outlier = 0 " "ORDER BY topological_ordering DESC, stream_ordering DESC LIMIT ? " ) From d5ae67e67d90e745b8eb06661af60370c68f813b Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Tue, 6 Jan 2015 16:05:01 +0000 Subject: [PATCH 31/36] Fix typo where we used wrong var. --- synapse/util/logutils.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/synapse/util/logutils.py b/synapse/util/logutils.py index c4dfb69c5..fd9ac4d4d 100644 --- a/synapse/util/logutils.py +++ b/synapse/util/logutils.py @@ -102,7 +102,7 @@ def time_function(f): _log_debug_as_f( f, "[FUNC START] {%s-%d}", - (func_name, _TIME_FUNC_ID), + (func_name, id), ) r = f(*args, **kwargs) @@ -111,7 +111,7 @@ def time_function(f): _log_debug_as_f( f, "[FUNC END] {%s-%d} %f", - (func_name, _TIME_FUNC_ID, end-start,), + (func_name, id, end-start,), ) return r From 36a2a877e2920a20a679af96cbb1c4d041b89f96 Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Tue, 6 Jan 2015 16:34:26 +0000 Subject: [PATCH 32/36] Use time.time() instead of time.clock() --- synapse/storage/_base.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/synapse/storage/_base.py b/synapse/storage/_base.py index a0b7f943f..f660fc6ea 100644 --- a/synapse/storage/_base.py +++ b/synapse/storage/_base.py @@ -64,7 +64,7 @@ class LoggingTransaction(object): # Don't let logging failures stop SQL from working pass - start = time.clock() * 1000 + start = time.time() * 1000 try: return self.txn.execute( sql, *args, **kwargs @@ -73,7 +73,7 @@ class LoggingTransaction(object): logger.exception("[SQL FAIL] {%s}", self.name) raise finally: - end = time.clock() * 1000 + end = time.time() * 1000 sql_logger.debug("[SQL time] {%s} %f", self.name, end - start) @@ -93,7 +93,7 @@ class SQLBaseStore(object): def inner_func(txn, *args, **kwargs): with LoggingContext("runInteraction") as context: current_context.copy_to(context) - start = time.clock() * 1000 + start = time.time() * 1000 txn_id = SQLBaseStore._TXN_ID # We don't really need these to be unique, so lets stop it from @@ -109,7 +109,7 @@ class SQLBaseStore(object): logger.exception("[TXN FAIL] {%s}", name) raise finally: - end = time.clock() * 1000 + end = time.time() * 1000 transaction_logger.debug( "[TXN END] {%s} %f", name, end - start From dfa05f0cd69779f21ae9b1085c40079440f87d3b Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Tue, 6 Jan 2015 18:51:03 +0000 Subject: [PATCH 33/36] Optimize FrozenEvent creation --- synapse/events/__init__.py | 20 +++++++++++++------- synapse/util/frozenutils.py | 5 +++-- 2 files changed, 16 insertions(+), 9 deletions(-) diff --git a/synapse/events/__init__.py b/synapse/events/__init__.py index 5dca04d92..6030c5887 100644 --- a/synapse/events/__init__.py +++ b/synapse/events/__init__.py @@ -20,7 +20,7 @@ import copy class _EventInternalMetadata(object): def __init__(self, internal_metadata_dict): - self.__dict__ = copy.deepcopy(internal_metadata_dict) + self.__dict__ = internal_metadata_dict def get_dict(self): return dict(self.__dict__) @@ -49,10 +49,10 @@ def _event_dict_property(key): class EventBase(object): def __init__(self, event_dict, signatures={}, unsigned={}, internal_metadata_dict={}): - self.signatures = copy.deepcopy(signatures) - self.unsigned = copy.deepcopy(unsigned) + self.signatures = signatures + self.unsigned = unsigned - self._event_dict = copy.deepcopy(event_dict) + self._event_dict = event_dict self.internal_metadata = _EventInternalMetadata( internal_metadata_dict @@ -112,10 +112,16 @@ class EventBase(object): class FrozenEvent(EventBase): def __init__(self, event_dict, internal_metadata_dict={}): - event_dict = copy.deepcopy(event_dict) + event_dict = dict(event_dict) - signatures = copy.deepcopy(event_dict.pop("signatures", {})) - unsigned = copy.deepcopy(event_dict.pop("unsigned", {})) + # Signatures is a dict of dicts, and this is faster than doing a + # copy.deepcopy + signatures = { + name: {sig_id: sig for sig_id, sig in sigs.items()} + for name, sigs in event_dict.pop("signatures", {}).items() + } + + unsigned = dict(event_dict.pop("unsigned", {})) frozen_dict = freeze(event_dict) diff --git a/synapse/util/frozenutils.py b/synapse/util/frozenutils.py index 061f79d79..a13a2015e 100644 --- a/synapse/util/frozenutils.py +++ b/synapse/util/frozenutils.py @@ -17,10 +17,11 @@ from frozendict import frozendict def freeze(o): - if isinstance(o, dict) or isinstance(o, frozendict): + t = type(o) + if t is dict: return frozendict({k: freeze(v) for k, v in o.items()}) - if isinstance(o, basestring): + if t is str or t is unicode: return o try: From a039e2544c5093f4f30147c063e97a2f31cbfe3a Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Wed, 7 Jan 2015 09:48:03 +0000 Subject: [PATCH 34/36] Remove unused import --- synapse/events/__init__.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/synapse/events/__init__.py b/synapse/events/__init__.py index 6030c5887..4252e5ab5 100644 --- a/synapse/events/__init__.py +++ b/synapse/events/__init__.py @@ -15,8 +15,6 @@ from synapse.util.frozenutils import freeze, unfreeze -import copy - class _EventInternalMetadata(object): def __init__(self, internal_metadata_dict): From 89fc09c3d11b3574919a311dbf68816116873abb Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Wed, 7 Jan 2015 13:56:56 +0000 Subject: [PATCH 35/36] Bump version and changelog --- CHANGES.rst | 8 ++++++++ VERSION | 2 +- synapse/__init__.py | 2 +- 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 813ad364e..3bd69367b 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,3 +1,11 @@ +Changes in synapse 0.6.1 (2015-01-07) +===================================== + + * Various optimization to improve performance of event sending and initial + sync. + * Media repository now includes a Content-Length header on media downloads. + * Improve quality of thumbnails by changing resizing algorithm. + Changes in synapse 0.6.0 (2014-12-16) ===================================== diff --git a/VERSION b/VERSION index a918a2aa1..ee6cdce3c 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.6.0 +0.6.1 diff --git a/synapse/__init__.py b/synapse/__init__.py index 06167e3c1..c3f1ac63b 100644 --- a/synapse/__init__.py +++ b/synapse/__init__.py @@ -16,4 +16,4 @@ """ This is a reference implementation of a synapse home server. """ -__version__ = "0.6.0" +__version__ = "0.6.1" From 72d8d1265b8316878d8d61ba8a7781c4ac0764d3 Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Wed, 7 Jan 2015 14:16:38 +0000 Subject: [PATCH 36/36] Improve change log --- CHANGES.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 3bd69367b..297ae914f 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,8 +1,8 @@ Changes in synapse 0.6.1 (2015-01-07) ===================================== - * Various optimization to improve performance of event sending and initial - sync. + * Major optimizations to improve performance of initial sync and event sending + in large rooms (by up to 10x) * Media repository now includes a Content-Length header on media downloads. * Improve quality of thumbnails by changing resizing algorithm.