From 454e688c3d626f8e2413d8c0d8acc65c204d8981 Mon Sep 17 00:00:00 2001 From: Jakob Borg Date: Thu, 26 Mar 2015 23:26:51 +0100 Subject: [PATCH] Push model data instead of pull (fixes #1434) --- cmd/syncthing/gui.go | 25 ++- cmd/syncthing/main.go | 11 +- cmd/syncthing/summarysvc.go | 159 ++++++++++++++++++ .../core/controllers/eventController.js | 6 +- .../core/controllers/syncthingController.js | 80 +++++---- internal/auto/gui.files.go | 4 +- internal/events/events.go | 6 + 7 files changed, 244 insertions(+), 47 deletions(-) create mode 100644 cmd/syncthing/summarysvc.go diff --git a/cmd/syncthing/gui.go b/cmd/syncthing/gui.go index 9a4b6f930..7966668ec 100644 --- a/cmd/syncthing/gui.go +++ b/cmd/syncthing/gui.go @@ -49,6 +49,11 @@ var ( eventSub *events.BufferedSubscription ) +var ( + lastEventRequest time.Time + lastEventRequestMut sync.Mutex +) + func init() { l.AddHandler(logger.LevelWarn, showGuiError) sub := events.Default.Subscribe(events.AllEvents) @@ -179,6 +184,9 @@ func startGUI(cfg config.GUIConfiguration, assetDir string, m *model.Model) erro ReadTimeout: 10 * time.Second, } + csrv := &folderSummarySvc{model: m} + go csrv.Serve() + go func() { err := srv.Serve(listener) if err != nil { @@ -293,8 +301,14 @@ func restGetCompletion(m *model.Model, w http.ResponseWriter, r *http.Request) { } func restGetModel(m *model.Model, w http.ResponseWriter, r *http.Request) { - var qs = r.URL.Query() - var folder = qs.Get("folder") + qs := r.URL.Query() + folder := qs.Get("folder") + res := folderSummary(m, folder) + w.Header().Set("Content-Type", "application/json; charset=utf-8") + json.NewEncoder(w).Encode(res) +} + +func folderSummary(m *model.Model, folder string) map[string]interface{} { var res = make(map[string]interface{}) res["invalid"] = cfg.Folders()[folder].Invalid @@ -322,8 +336,7 @@ func restGetModel(m *model.Model, w http.ResponseWriter, r *http.Request) { } } - w.Header().Set("Content-Type", "application/json; charset=utf-8") - json.NewEncoder(w).Encode(res) + return res } func restPostOverride(m *model.Model, w http.ResponseWriter, r *http.Request) { @@ -598,6 +611,10 @@ func restGetEvents(w http.ResponseWriter, r *http.Request) { since, _ := strconv.Atoi(sinceStr) limit, _ := strconv.Atoi(limitStr) + lastEventRequestMut.Lock() + lastEventRequest = time.Now() + lastEventRequestMut.Unlock() + w.Header().Set("Content-Type", "application/json; charset=utf-8") // Flush before blocking, to indicate that we've received the request diff --git a/cmd/syncthing/main.go b/cmd/syncthing/main.go index f15e847bb..94b65336d 100644 --- a/cmd/syncthing/main.go +++ b/cmd/syncthing/main.go @@ -61,7 +61,10 @@ const ( exitUpgrading = 4 ) -const bepProtocolName = "bep/1.0" +const ( + bepProtocolName = "bep/1.0" + pingEventInterval = time.Minute +) var l = logger.DefaultLogger @@ -613,7 +616,7 @@ func syncthingMain() { } events.Default.Log(events.StartupComplete, nil) - go generateEvents() + go generatePingEvents() code := <-stop @@ -701,9 +704,9 @@ func defaultConfig(myName string) config.Configuration { return newCfg } -func generateEvents() { +func generatePingEvents() { for { - time.Sleep(300 * time.Second) + time.Sleep(pingEventInterval) events.Default.Log(events.Ping, nil) } } diff --git a/cmd/syncthing/summarysvc.go b/cmd/syncthing/summarysvc.go new file mode 100644 index 000000000..9120e524e --- /dev/null +++ b/cmd/syncthing/summarysvc.go @@ -0,0 +1,159 @@ +// Copyright (C) 2015 The Syncthing Authors. +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this file, +// You can obtain one at http://mozilla.org/MPL/2.0/. + +package main + +import ( + "sync" + "time" + + "github.com/syncthing/syncthing/internal/events" + "github.com/syncthing/syncthing/internal/model" + "github.com/thejerf/suture" +) + +// The folderSummarySvc adds summary information events (FolderSummary and +// FolderCompletion) into the event stream at certain intervals. +type folderSummarySvc struct { + model *model.Model + srv suture.Service + stop chan struct{} + + // For keeping track of folders to recalculate for + foldersMut sync.Mutex + folders map[string]struct{} +} + +func (c *folderSummarySvc) Serve() { + srv := suture.NewSimple("folderSummarySvc") + srv.Add(serviceFunc(c.listenForUpdates)) + srv.Add(serviceFunc(c.calculateSummaries)) + + c.stop = make(chan struct{}) + c.folders = make(map[string]struct{}) + c.srv = srv + + srv.Serve() +} + +func (c *folderSummarySvc) Stop() { + // c.srv.Stop() is mostly a no-op here, but we need to call it anyway so + // c.srv doesn't try to restart the serviceFuncs when they exit after we + // close the stop channel. + c.srv.Stop() + close(c.stop) +} + +// listenForUpdates subscribes to the event bus and makes note of folders that +// need their data recalculated. +func (c *folderSummarySvc) listenForUpdates() { + sub := events.Default.Subscribe(events.LocalIndexUpdated | events.RemoteIndexUpdated) + defer events.Default.Unsubscribe(sub) + + for { + // This loop needs to be fast so we don't miss too many events. + + select { + case ev := <-sub.C(): + // Whenever the local or remote index is updated for a given + // folder we make a note of it. + + data := ev.Data.(map[string]interface{}) + folder := data["folder"].(string) + c.foldersMut.Lock() + c.folders[folder] = struct{}{} + c.foldersMut.Unlock() + + case <-c.stop: + return + } + } +} + +// calculateSummaries periodically recalculates folder summaries and +// completion percentage, and sends the results on the event bus. +func (c *folderSummarySvc) calculateSummaries() { + const pumpInterval = 2 * time.Second + pump := time.NewTimer(pumpInterval) + + for { + select { + case <-pump.C: + // We only recalculate sumamries if someone is listening to events + // (a request to /rest/events has been made within the last + // pingEventInterval). + + lastEventRequestMut.Lock() + // XXX: Reaching out to a global var here is very ugly :( Should + // we make the gui stuff a proper object with methods on it that + // we can query about this kind of thing? + last := lastEventRequest + lastEventRequestMut.Unlock() + + t0 := time.Now() + if time.Since(last) < pingEventInterval { + for _, folder := range c.foldersToHandle() { + // The folder summary contains how many bytes, files etc + // are in the folder and how in sync we are. + data := folderSummary(c.model, folder) + events.Default.Log(events.FolderSummary, map[string]interface{}{ + "folder": folder, + "summary": data, + }) + + for _, devCfg := range cfg.Folders()[folder].Devices { + if devCfg.DeviceID.Equals(myID) { + // We already know about ourselves. + continue + } + if !c.model.ConnectedTo(devCfg.DeviceID) { + // We're not interested in disconnected devices. + continue + } + + // Get completion percentage of this folder for the + // remote device. + comp := c.model.Completion(devCfg.DeviceID, folder) + events.Default.Log(events.FolderCompletion, map[string]interface{}{ + "folder": folder, + "device": devCfg.DeviceID.String(), + "completion": comp, + }) + } + } + } + + // We don't want to spend all our time calculating summaries. Lets + // set an arbitrary limit at not spending more than about 30% of + // our time here... + wait := 2*time.Since(t0) + pumpInterval + pump.Reset(wait) + + case <-c.stop: + return + } + } +} + +// foldersToHandle returns the list of folders needing a summary update, and +// clears the list. +func (c *folderSummarySvc) foldersToHandle() []string { + c.foldersMut.Lock() + res := make([]string, 0, len(c.folders)) + for folder := range c.folders { + res = append(res, folder) + delete(c.folders, folder) + } + c.foldersMut.Unlock() + return res +} + +// serviceFunc wraps a function to create a suture.Service without stop +// functionality. +type serviceFunc func() + +func (f serviceFunc) Serve() { f() } +func (f serviceFunc) Stop() {} diff --git a/gui/scripts/syncthing/core/controllers/eventController.js b/gui/scripts/syncthing/core/controllers/eventController.js index 85c30763d..f72da69ba 100644 --- a/gui/scripts/syncthing/core/controllers/eventController.js +++ b/gui/scripts/syncthing/core/controllers/eventController.js @@ -1,3 +1,5 @@ +var debugEvents = false; + angular.module('syncthing.core') .controller('EventController', function ($scope, $http) { 'use strict'; @@ -20,7 +22,9 @@ angular.module('syncthing.core') if (lastID > 0) { data.forEach(function (event) { - console.log("event", event.id, event.type, event.data); + if (debugEvents) { + console.log("event", event.id, event.type, event.data); + } $scope.$emit(event.type, event); }); } diff --git a/gui/scripts/syncthing/core/controllers/syncthingController.js b/gui/scripts/syncthing/core/controllers/syncthingController.js index 610f830ba..e7615d82e 100644 --- a/gui/scripts/syncthing/core/controllers/syncthingController.js +++ b/gui/scripts/syncthing/core/controllers/syncthingController.js @@ -140,19 +140,11 @@ angular.module('syncthing.core') $scope.$on('LocalIndexUpdated', function (event, arg) { var data = arg.data; - refreshFolder(data.folder); refreshFolderStats(); - - // Update completion status for all devices that we share this folder with. - $scope.folders[data.folder].devices.forEach(function (deviceCfg) { - refreshCompletion(deviceCfg.deviceID, data.folder); - }); }); $scope.$on('RemoteIndexUpdated', function (event, arg) { - var data = arg.data; - refreshFolder(data.folder); - refreshCompletion(data.device, data.folder); + // Nothing }); $scope.$on('DeviceDisconnected', function (event, arg) { @@ -215,7 +207,6 @@ angular.module('syncthing.core') var stats = arg.data; var progress = {}; for (var folder in stats) { - refreshFolder(folder); progress[folder] = {}; for (var file in stats[folder]) { var s = stats[folder][file]; @@ -241,7 +232,6 @@ angular.module('syncthing.core') } for (var folder in $scope.progress) { if (!(folder in progress)) { - refreshFolder(folder); if ($scope.neededFolder == folder) { refreshNeed(folder); } @@ -258,6 +248,30 @@ angular.module('syncthing.core') console.log("DownloadProgress", $scope.progress); }); + $scope.$on('FolderSummary', function (event, arg) { + var data = arg.data; + $scope.model[data.folder] = data.summary; + }); + + $scope.$on('FolderCompletion', function (event, arg) { + var data = arg.data; + if (!$scope.completion[data.device]) { + $scope.completion[data.device] = {}; + } + $scope.completion[data.device][data.folder] = data.completion; + + var tot = 0, + cnt = 0; + for (var cmp in $scope.completion[data.device]) { + if (cmp === "_total") { + continue; + } + tot += $scope.completion[data.device][cmp]; + cnt += 1; + } + $scope.completion[data.device]._total = tot / cnt; + }); + $scope.emitHTTPError = function (data, status, headers, config) { $scope.$emit('HTTPError', {data: data, status: status, headers: headers, config: config}); }; @@ -325,31 +339,25 @@ angular.module('syncthing.core') return; } - var key = "refreshCompletion" + device + folder; - if (!debouncedFuncs[key]) { - debouncedFuncs[key] = debounce(function () { - $http.get(urlbase + '/completion?device=' + device + '&folder=' + encodeURIComponent(folder)).success(function (data) { - if (!$scope.completion[device]) { - $scope.completion[device] = {}; - } - $scope.completion[device][folder] = data.completion; + $http.get(urlbase + '/completion?device=' + device + '&folder=' + encodeURIComponent(folder)).success(function (data) { + if (!$scope.completion[device]) { + $scope.completion[device] = {}; + } + $scope.completion[device][folder] = data.completion; - var tot = 0, - cnt = 0; - for (var cmp in $scope.completion[device]) { - if (cmp === "_total") { - continue; - } - tot += $scope.completion[device][cmp]; - cnt += 1; - } - $scope.completion[device]._total = tot / cnt; + var tot = 0, + cnt = 0; + for (var cmp in $scope.completion[device]) { + if (cmp === "_total") { + continue; + } + tot += $scope.completion[device][cmp]; + cnt += 1; + } + $scope.completion[device]._total = tot / cnt; - console.log("refreshCompletion", device, folder, $scope.completion[device]); - }).error($scope.emitHTTPError); - }, 1000, true); - } - debouncedFuncs[key](); + console.log("refreshCompletion", device, folder, $scope.completion[device]); + }).error($scope.emitHTTPError); } function refreshConnectionStats() { @@ -412,7 +420,7 @@ angular.module('syncthing.core') } console.log("refreshDeviceStats", data); }).error($scope.emitHTTPError); - }, 500); + }, 2500); var refreshFolderStats = debounce(function () { $http.get(urlbase + "/stats/folder").success(function (data) { @@ -424,7 +432,7 @@ angular.module('syncthing.core') } console.log("refreshfolderStats", data); }).error($scope.emitHTTPError); - }, 500); + }, 2500); $scope.refresh = function () { refreshSystem(); diff --git a/internal/auto/gui.files.go b/internal/auto/gui.files.go index 5da44d1a5..1eca14ea3 100644 --- a/internal/auto/gui.files.go +++ b/internal/auto/gui.files.go @@ -187,12 +187,12 @@ func Assets() map[string][]byte { bs, _ = ioutil.ReadAll(gr) assets["scripts/syncthing/app.js"] = bs - bs, _ = base64.StdEncoding.DecodeString("H4sIAAAJbogA/7RUT4/aPhC98ynmt0JK0C8NUKknRHtod6U99dCteqh6MMkkseTYyB4vQhXfvWOHDRCCtqpaXwjz/z2/sdC1V8LmrSm9wjRxe11QI3WdF8ZiMpsAH/7WZI1SaNPk/hk1fewNSQaV5xxpNKRTV5gtZjBtiLYz+Bmzw0m8Q3BkZUHJatKbu/hcCUexLKxBe6VWfcCzsBC8j5/YtTjLDA7niwKde9DsO81QChLnrcOZz+Fbgxq+vKADi46EJQe7RioEahCUYfuWQQU/I9bYVZQOpB6W21pTcw0XMzfW7Bxa4GBnWoStElQZ2zpuQ95qBwLeLhaQOqmL2GxYrkFRonXQCAcb5Ekr5V2DJewkNbFHV4nnKjHUmmWdSxsIgPNhwaeGx94IJwuh1B5aFDrMKigWO0PXd2Q0SBkIXXYhnDcsuuNYbQhEQT6W5RtgDiqvLvvLCtL/xu4hHLTW2Afd3dPqyt3hvLQfJhd/j6qZYispTb4+ftZ8ZazV1eRqiqN23sNibJTIHF/UvSia9KQgDFIciw+HqXNGsWZNnd7FyLsM4m8uy5cv2oct6L5v4LwCcpU5knSY/Q4x5+sUun+POBXqmgXzBpY/Lov0+zVMZ0ADSlkhT7JF4+mMrjGm4gOQ10ipt4pliPA/JPMIy32IW7BO2NS1no2Skx/XO+3X/EZcFFR6lNWQoAze8bKcjIfBG3JMe/UFudJcVY2K7u8wpCS3WS+Tf8vMcnGTmj+Y7rWpRqcJgv4FAAD//wEAAP//QKHce4MGAAA=") + bs, _ = base64.StdEncoding.DecodeString("H4sIAAAJbogA/7RUQYvbPBC951fMtyzY4XO9SaGnkPbQ7sKeeuiWHkoPij22BbIUNNKGUPLfO5ITr+M4bCmtLnFGM2/ePD3pWVgocePr+2fUjmANlVCEq9lM6NorYfPWlF5hmtBeF66Rus4LYzGZz4AXf2tnjVJo0yRCfOwDSQaV5xppNKS3VJgtZnDbOLedw89YHVbiCYGclYVLuOsp3OXnSpCLsExMe6VWfUIgHnYfP/HWYlAZNsgXBRI96DBPz6EUTgxbh3V3B98a1PDlNB1YJCcsS7FrpEJwDYIyHN/yUGGfJ9bYIUoCqcdwW2tqxqBYubFmR2iBk8m0CFslXGVsS9zGeasJBLxdLCAlqYvYbAzXoCjREjSCYIPMtFKeGixhJ10Te3RIzKvEgDXPui1tIAycjwGfGqa9ESQLodQeWhQ6cBUugg2m6zvyNOgyELrsUrhuDLrjXG0ciML5CMsnwBpUXp33lxWk/02dQ1horbEPujun1cV2N+d5/DA7+3t0zS220qXJ18fPmo+MvbqaXbA4euc9LKaoROX4oO5F0aQvDsJgxan8E+rgKl1LC4tVJqPY3qZObyLoTQbxN5fl6cvtw4Xpvq9I0kkwFT1T4gJvAuow/x1lh/cxcPoehVKoa3bcG1j+OAfpL+i4nMccnQlb7Em2aLwb6D2lYXxB8hpd6q1iHyP8D8ldHIs+xGu0TjjUtZ5PipMf34e0fyeu5EVHpkdfjgXK4B3ftpfgYfQIHctefYIuTFtVk679OwopyW3Wy+TfKrNcXJXmD9i9xmqSTTD0LwAAAP//AQAA//8bMA983gYAAA==") gr, _ = gzip.NewReader(bytes.NewReader(bs)) bs, _ = ioutil.ReadAll(gr) assets["scripts/syncthing/core/controllers/eventController.js"] = bs - bs, _ = base64.StdEncoding.DecodeString("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") + bs, _ = base64.StdEncoding.DecodeString("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") gr, _ = gzip.NewReader(bytes.NewReader(bs)) bs, _ = ioutil.ReadAll(gr) assets["scripts/syncthing/core/controllers/syncthingController.js"] = bs diff --git a/internal/events/events.go b/internal/events/events.go index 7ccadd3ca..3f22c4f41 100644 --- a/internal/events/events.go +++ b/internal/events/events.go @@ -31,6 +31,8 @@ const ( FolderRejected ConfigSaved DownloadProgress + FolderSummary + FolderCompletion AllEvents = (1 << iota) - 1 ) @@ -67,6 +69,10 @@ func (t EventType) String() string { return "ConfigSaved" case DownloadProgress: return "DownloadProgress" + case FolderSummary: + return "FolderSummary" + case FolderCompletion: + return "FolderCompletion" default: return "Unknown" }