From fd0a147ae6dd79988bd068135360572e446d0a2b Mon Sep 17 00:00:00 2001 From: Audrius Butkevicius Date: Mon, 1 Dec 2014 19:23:06 +0000 Subject: [PATCH 1/7] Add job queue (fixes #629) Request to terminate currently ongoing downloads and jump to the bumped file incoming in 3, 2, 1. Also, has a slightly strange effect where we pop a job off the queue, but the copyChannel is still busy and blocks, though it gets moved to the progress slice in the jobqueue, and looks like it's in progress which it isn't as it's waiting to be picked up from the copyChan. As a result, the progress emitter doesn't register on the task, and hence the file doesn't have a progress bar, but cannot be replaced by a bump. I guess I can fix progress bar issue by moving the progressEmiter.Register just before passing the file to the copyChan, but then we are back to the initial problem of a file with a progress bar, but no progress happening as it's stuck on write to copyChan I checked if there is a way to check for channel writeability (before popping) but got struck by lightning just for bringing the idea up in #go-nuts. My ideal scenario would be to check if copyChan is writeable, pop job from the queue and shove it down handleFile. This way jobs would stay in the queue while they cannot be handled, meaning that the `Bump` could bring your file up higher. --- cmd/syncthing/gui.go | 42 +++++-- gui/index.html | 40 +++++-- .../core/controllers/syncthingController.js | 9 ++ internal/auto/gui.files.go | 4 +- internal/model/model.go | 59 ++++++++-- internal/model/puller.go | 43 ++++++- internal/model/queue.go | 106 ++++++++++++++++++ internal/model/scanner.go | 8 ++ internal/protocol/message.go | 11 ++ 9 files changed, 282 insertions(+), 40 deletions(-) create mode 100644 internal/model/queue.go diff --git a/cmd/syncthing/gui.go b/cmd/syncthing/gui.go index 98a80933..b0f5ac4c 100644 --- a/cmd/syncthing/gui.go +++ b/cmd/syncthing/gui.go @@ -149,6 +149,7 @@ func startGUI(cfg config.GUIConfiguration, assetDir string, m *model.Model) erro postRestMux.HandleFunc("/rest/shutdown", restPostShutdown) postRestMux.HandleFunc("/rest/upgrade", restPostUpgrade) postRestMux.HandleFunc("/rest/scan", withModel(m, restPostScan)) + postRestMux.HandleFunc("/rest/bump", withModel(m, restPostBump)) // A handler that splits requests between the two above and disables // caching @@ -314,19 +315,12 @@ func restGetNeed(m *model.Model, w http.ResponseWriter, r *http.Request) { var qs = r.URL.Query() var folder = qs.Get("folder") - files := m.NeedFolderFilesLimited(folder, 100) // max 100 files + progress, queued, rest := m.NeedFolderFiles(folder, 100) // Convert the struct to a more loose structure, and inject the size. - output := make([]map[string]interface{}, 0, len(files)) - for _, file := range files { - output = append(output, map[string]interface{}{ - "Name": file.Name, - "Flags": file.Flags, - "Modified": file.Modified, - "Version": file.Version, - "LocalVersion": file.LocalVersion, - "NumBlocks": file.NumBlocks, - "Size": protocol.BlocksToSize(file.NumBlocks), - }) + output := map[string][]map[string]interface{}{ + "progress": toNeedSlice(progress), + "queued": toNeedSlice(queued), + "rest": toNeedSlice(rest), } w.Header().Set("Content-Type", "application/json; charset=utf-8") @@ -650,6 +644,14 @@ func restPostScan(m *model.Model, w http.ResponseWriter, r *http.Request) { } } +func restPostBump(m *model.Model, w http.ResponseWriter, r *http.Request) { + qs := r.URL.Query() + folder := qs.Get("folder") + file := qs.Get("file") + m.Bump(folder, file) + restGetNeed(m, w, r) +} + func getQR(w http.ResponseWriter, r *http.Request) { var qs = r.URL.Query() var text = qs.Get("text") @@ -775,3 +777,19 @@ func mimeTypeForFile(file string) string { return mime.TypeByExtension(ext) } } + +func toNeedSlice(files []protocol.FileInfoTruncated) []map[string]interface{} { + output := make([]map[string]interface{}, len(files)) + for i, file := range files { + output[i] = map[string]interface{}{ + "Name": file.Name, + "Flags": file.Flags, + "Modified": file.Modified, + "Version": file.Version, + "LocalVersion": file.LocalVersion, + "NumBlocks": file.NumBlocks, + "Size": protocol.BlocksToSize(file.NumBlocks), + } + } + return output +} diff --git a/gui/index.html b/gui/index.html index 890c8da3..b1fb4251 100644 --- a/gui/index.html +++ b/gui/index.html @@ -801,21 +801,37 @@
- + - + + + + + + + + + + +
{{needActions[a]}} {{f.Name | basename}} - -
-
-
-
-
-
- {{progress[neededFolder][f.Name].BytesDone | binary}}B / {{progress[neededFolder][f.Name].BytesTotal | binary}}B -
-
+
+
+
+
+
+
+
+ + {{progress[neededFolder][f.Name].BytesDone | binary}}B / {{progress[neededFolder][f.Name].BytesTotal | binary}}B + +
+ {{f.Size | binary}}B +
{{needActions[a]}}{{f.Name | basename}} + + {{f.Size | binary}}B +
{{needActions[a]}}{{f.Name | basename}}{{f.Size | binary}}B
diff --git a/gui/scripts/syncthing/core/controllers/syncthingController.js b/gui/scripts/syncthing/core/controllers/syncthingController.js index 14142c1f..387030d3 100644 --- a/gui/scripts/syncthing/core/controllers/syncthingController.js +++ b/gui/scripts/syncthing/core/controllers/syncthingController.js @@ -1056,6 +1056,15 @@ angular.module('syncthing.core') $http.post(urlbase + "/scan?folder=" + encodeURIComponent(folder)); }; + $scope.bumpFile = function (folder, file) { + $http.post(urlbase + "/bump?folder=" + encodeURIComponent(folder) + "&file=" + encodeURIComponent(file)).success(function (data) { + if ($scope.neededFolder == folder) { + console.log("bumpFile", folder, data); + $scope.needed = data; + } + }); + }; + // pseudo main. called on all definitions assigned initController(); }); diff --git a/internal/auto/gui.files.go b/internal/auto/gui.files.go index 6f7d1b08..7b5b2ad0 100644 --- a/internal/auto/gui.files.go +++ b/internal/auto/gui.files.go @@ -147,7 +147,7 @@ func Assets() map[string][]byte { bs, _ = ioutil.ReadAll(gr) assets["assets/lang/valid-langs.js"] = bs - bs, _ = base64.StdEncoding.DecodeString("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") + bs, _ = base64.StdEncoding.DecodeString("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") gr, _ = gzip.NewReader(bytes.NewBuffer(bs)) bs, _ = ioutil.ReadAll(gr) assets["index.html"] = bs @@ -167,7 +167,7 @@ func Assets() map[string][]byte { 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.NewBuffer(bs)) bs, _ = ioutil.ReadAll(gr) assets["scripts/syncthing/core/controllers/syncthingController.js"] = bs diff --git a/internal/model/model.go b/internal/model/model.go index c1a64d1e..a1a3c770 100644 --- a/internal/model/model.go +++ b/internal/model/model.go @@ -79,6 +79,8 @@ const ( type service interface { Serve() Stop() + Jobs() ([]protocol.FileInfoTruncated, []protocol.FileInfoTruncated) // In progress, Queued + Bump(string) } type Model struct { @@ -416,22 +418,46 @@ func (m *Model) NeedSize(folder string) (files int, bytes int64) { return } -// NeedFiles returns the list of currently needed files, stopping at maxFiles -// files. Limit <= 0 is ignored. -func (m *Model) NeedFolderFilesLimited(folder string, maxFiles int) []protocol.FileInfoTruncated { +// NeedFiles returns the list of currently needed files in progress, queued, +// and to be queued on next puller iteration. Also takes a soft cap which is +// only respected when adding files from the model rather than the runner queue. +func (m *Model) NeedFolderFiles(folder string, max int) ([]protocol.FileInfoTruncated, []protocol.FileInfoTruncated, []protocol.FileInfoTruncated) { defer m.leveldbPanicWorkaround() m.fmut.RLock() defer m.fmut.RUnlock() if rf, ok := m.folderFiles[folder]; ok { - fs := make([]protocol.FileInfoTruncated, 0, maxFiles) - rf.WithNeedTruncated(protocol.LocalDeviceID, func(f protocol.FileIntf) bool { - fs = append(fs, f.(protocol.FileInfoTruncated)) - return maxFiles <= 0 || len(fs) < maxFiles - }) - return fs + progress := []protocol.FileInfoTruncated{} + queued := []protocol.FileInfoTruncated{} + rest := []protocol.FileInfoTruncated{} + seen := map[string]struct{}{} + + runner, ok := m.folderRunners[folder] + if ok { + progress, queued = runner.Jobs() + seen = make(map[string]struct{}, len(progress)+len(queued)) + for _, file := range progress { + seen[file.Name] = struct{}{} + } + for _, file := range queued { + seen[file.Name] = struct{}{} + } + } + left := max - len(progress) - len(queued) + if max < 1 || left > 0 { + rf.WithNeedTruncated(protocol.LocalDeviceID, func(f protocol.FileIntf) bool { + left-- + ft := f.(protocol.FileInfoTruncated) + _, ok := seen[ft.Name] + if !ok { + rest = append(rest, ft) + } + return max < 1 || left > 0 + }) + } + return progress, queued, rest } - return nil + return nil, nil, nil } // Index is called when a new device is connected and we receive their full index. @@ -1336,7 +1362,7 @@ func (m *Model) RemoteLocalVersion(folder string) uint64 { return ver } -func (m *Model) availability(folder string, file string) []protocol.DeviceID { +func (m *Model) availability(folder, file string) []protocol.DeviceID { // Acquire this lock first, as the value returned from foldersFiles can // gen heavily modified on Close() m.pmut.RLock() @@ -1359,6 +1385,17 @@ func (m *Model) availability(folder string, file string) []protocol.DeviceID { return availableDevices } +// Bump the given files priority in the job queue +func (m *Model) Bump(folder, file string) { + m.pmut.RLock() + defer m.pmut.RUnlock() + + runner, ok := m.folderRunners[folder] + if ok { + runner.Bump(file) + } +} + func (m *Model) String() string { return fmt.Sprintf("model@%p", m) } diff --git a/internal/model/puller.go b/internal/model/puller.go index 1d9fea68..e908dfa5 100644 --- a/internal/model/puller.go +++ b/internal/model/puller.go @@ -78,6 +78,7 @@ type Puller struct { copiers int pullers int finishers int + queue *JobQueue } // Serve will run scans and pulls. It will return when Stop()ed or on a @@ -89,6 +90,7 @@ func (p *Puller) Serve() { } p.stop = make(chan struct{}) + p.queue = NewJobQueue() pullTimer := time.NewTimer(checkPullIntv) scanTimer := time.NewTimer(time.Millisecond) // The first scan should be done immediately. @@ -337,15 +339,22 @@ func (p *Puller) pullerIteration(checksum bool, ignores *ignore.Matcher) int { p.handleDir(file) default: // A new or changed file or symlink. This is the only case where we - // do stuff in the background; the other three are done - // synchronously. - p.handleFile(file, copyChan, finisherChan) + // do stuff concurrently in the background + p.queue.Push(&file) } changed++ return true }) + for { + f := p.queue.Pop() + if f == nil { + break + } + p.handleFile(*f, copyChan, finisherChan) + } + // Signal copy and puller routines that we are done with the in data for // this iteration. Wait for them to finish. close(copyChan) @@ -483,6 +492,7 @@ func (p *Puller) handleFile(file protocol.FileInfo, copyChan chan<- copyBlocksSt if debug { l.Debugln(p, "taking shortcut on", file.Name) } + p.queue.Done(&file) if file.IsSymlink() { p.shortcutSymlink(curFile, file) } else { @@ -850,6 +860,7 @@ func (p *Puller) finisherRoutine(in <-chan *sharedPullerState) { continue } + p.queue.Done(&state.file) p.performFinish(state) p.model.receivedFile(p.folder, state.file.Name) if p.progressEmitter != nil { @@ -859,6 +870,32 @@ func (p *Puller) finisherRoutine(in <-chan *sharedPullerState) { } } +// Moves the given filename to the front of the job queue +func (p *Puller) Bump(filename string) { + p.queue.Bump(filename) +} + +func (p *Puller) Jobs() ([]protocol.FileInfoTruncated, []protocol.FileInfoTruncated) { + return p.queue.Jobs() +} + +// clean deletes orphaned temporary files +func (p *Puller) clean() { + keep := time.Duration(p.model.cfg.Options().KeepTemporariesH) * time.Hour + now := time.Now() + filepath.Walk(p.dir, func(path string, info os.FileInfo, err error) error { + if err != nil { + return err + } + + if info.Mode().IsRegular() && defTempNamer.IsTemporary(path) && info.ModTime().Add(keep).Before(now) { + os.Remove(path) + } + + return nil + }) +} + func invalidateFolder(cfg *config.Configuration, folderID string, err error) { for i := range cfg.Folders { folder := &cfg.Folders[i] diff --git a/internal/model/queue.go b/internal/model/queue.go new file mode 100644 index 00000000..fd4f4d55 --- /dev/null +++ b/internal/model/queue.go @@ -0,0 +1,106 @@ +// Copyright (C) 2014 The Syncthing Authors. +// +// This program is free software: you can redistribute it and/or modify it +// under the terms of the GNU General Public License as published by the Free +// Software Foundation, either version 3 of the License, or (at your option) +// any later version. +// +// This program is distributed in the hope that it will be useful, but WITHOUT +// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for +// more details. +// +// You should have received a copy of the GNU General Public License along +// with this program. If not, see . + +package model + +import ( + "container/list" + "sync" + + "github.com/syncthing/syncthing/internal/protocol" +) + +type JobQueue struct { + progress []*protocol.FileInfo + + queued *list.List + lookup map[string]*list.Element // O(1) lookups + + mut sync.Mutex +} + +func NewJobQueue() *JobQueue { + return &JobQueue{ + progress: []*protocol.FileInfo{}, + queued: list.New(), + lookup: make(map[string]*list.Element), + } +} + +func (q *JobQueue) Push(file *protocol.FileInfo) { + q.mut.Lock() + defer q.mut.Unlock() + + q.lookup[file.Name] = q.queued.PushBack(file) +} + +func (q *JobQueue) Pop() *protocol.FileInfo { + q.mut.Lock() + defer q.mut.Unlock() + + if q.queued.Len() == 0 { + return nil + } + + f := q.queued.Remove(q.queued.Front()).(*protocol.FileInfo) + delete(q.lookup, f.Name) + q.progress = append(q.progress, f) + + return f +} + +func (q *JobQueue) Bump(filename string) { + q.mut.Lock() + defer q.mut.Unlock() + + ele, ok := q.lookup[filename] + if ok { + q.queued.MoveToFront(ele) + } +} + +func (q *JobQueue) Done(file *protocol.FileInfo) { + q.mut.Lock() + defer q.mut.Unlock() + + for i := range q.progress { + if q.progress[i].Name == file.Name { + copy(q.progress[i:], q.progress[i+1:]) + q.progress[len(q.progress)-1] = nil + q.progress = q.progress[:len(q.progress)-1] + return + } + } +} + +func (q *JobQueue) Jobs() ([]protocol.FileInfoTruncated, []protocol.FileInfoTruncated) { + q.mut.Lock() + defer q.mut.Unlock() + + progress := make([]protocol.FileInfoTruncated, len(q.progress)) + for i := range q.progress { + progress[i] = q.progress[i].ToTruncated() + } + + queued := make([]protocol.FileInfoTruncated, q.queued.Len()) + i := 0 + for e := q.queued.Front(); e != nil; e = e.Next() { + fi := e.Value.(*protocol.FileInfo) + queued[i] = fi.ToTruncated() + i++ + } + + return progress, queued +} diff --git a/internal/model/scanner.go b/internal/model/scanner.go index 98c1fc2d..6290aef3 100644 --- a/internal/model/scanner.go +++ b/internal/model/scanner.go @@ -18,6 +18,8 @@ package model import ( "fmt" "time" + + "github.com/syncthing/syncthing/internal/protocol" ) type Scanner struct { @@ -75,3 +77,9 @@ func (s *Scanner) Stop() { func (s *Scanner) String() string { return fmt.Sprintf("scanner/%s@%p", s.folder, s) } + +func (s *Scanner) Bump(string) {} + +func (s *Scanner) Jobs() ([]protocol.FileInfoTruncated, []protocol.FileInfoTruncated) { + return nil, nil +} diff --git a/internal/protocol/message.go b/internal/protocol/message.go index 8cc191d8..ae04a9da 100644 --- a/internal/protocol/message.go +++ b/internal/protocol/message.go @@ -69,6 +69,17 @@ func (f FileInfo) HasPermissionBits() bool { return f.Flags&FlagNoPermBits == 0 } +func (f FileInfo) ToTruncated() FileInfoTruncated { + return FileInfoTruncated{ + Name: f.Name, + Flags: f.Flags, + Modified: f.Modified, + Version: f.Version, + LocalVersion: f.LocalVersion, + NumBlocks: uint32(len(f.Blocks)), + } +} + // Used for unmarshalling a FileInfo structure but skipping the actual block list type FileInfoTruncated struct { Name string // max:8192 From b753f01ac12d8339d54f8b1822bc848cfabba13d Mon Sep 17 00:00:00 2001 From: Audrius Butkevicius Date: Mon, 1 Dec 2014 21:33:40 +0000 Subject: [PATCH 2/7] Add tests --- internal/model/queue_test.go | 133 +++++++++++++++++++++++++++++++++++ 1 file changed, 133 insertions(+) create mode 100644 internal/model/queue_test.go diff --git a/internal/model/queue_test.go b/internal/model/queue_test.go new file mode 100644 index 00000000..00d5ffd6 --- /dev/null +++ b/internal/model/queue_test.go @@ -0,0 +1,133 @@ +// Copyright (C) 2014 The Syncthing Authors. +// +// This program is free software: you can redistribute it and/or modify it +// under the terms of the GNU General Public License as published by the Free +// Software Foundation, either version 3 of the License, or (at your option) +// any later version. +// +// This program is distributed in the hope that it will be useful, but WITHOUT +// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for +// more details. +// +// You should have received a copy of the GNU General Public License along +// with this program. If not, see . + +package model + +import ( + "fmt" + "testing" + + "github.com/syncthing/syncthing/internal/protocol" +) + +var ( + f1 = &protocol.FileInfo{Name: "f1"} + f2 = &protocol.FileInfo{Name: "f2"} + f3 = &protocol.FileInfo{Name: "f3"} + f4 = &protocol.FileInfo{Name: "f4"} + f5 = &protocol.FileInfo{Name: "f5"} +) + +func TestJobQueue(t *testing.T) { + // Some random actions + q := NewJobQueue() + q.Push(f1) + q.Push(f2) + q.Push(f3) + q.Push(f4) + + progress, queued := q.Jobs() + if len(progress) != 0 || len(queued) != 4 { + t.Fatal("Wrong length") + } + + for i := 1; i < 5; i++ { + n := q.Pop() + if n == nil || n.Name != fmt.Sprintf("f%d", i) { + t.Fatal("Wrong element") + } + progress, queued = q.Jobs() + if len(progress) != 1 || len(queued) != 3 { + t.Fatal("Wrong length") + } + + q.Done(n) + progress, queued = q.Jobs() + if len(progress) != 0 || len(queued) != 3 { + t.Fatal("Wrong length", len(progress), len(queued)) + } + + q.Push(n) + progress, queued = q.Jobs() + if len(progress) != 0 || len(queued) != 4 { + t.Fatal("Wrong length") + } + + q.Done(f5) // Does not exist + progress, queued = q.Jobs() + if len(progress) != 0 || len(queued) != 4 { + t.Fatal("Wrong length") + } + } + + if len(q.progress) > 0 || len(q.lookup) != 4 || q.queued.Len() != 4 { + t.Fatal("Wrong length") + } + + for i := 4; i > 0; i-- { + progress, queued = q.Jobs() + if len(progress) != 4-i || len(queued) != i { + t.Fatal("Wrong length") + } + + s := fmt.Sprintf("f%d", i) + + q.Bump(s) + progress, queued = q.Jobs() + if len(progress) != 4-i || len(queued) != i { + t.Fatal("Wrong length") + } + + n := q.Pop() + if n == nil || n.Name != s { + t.Fatal("Wrong element") + } + progress, queued = q.Jobs() + if len(progress) != 5-i || len(queued) != i-1 { + t.Fatal("Wrong length") + } + + q.Done(f5) // Does not exist + progress, queued = q.Jobs() + if len(progress) != 5-i || len(queued) != i-1 { + t.Fatal("Wrong length") + } + } + + if len(q.progress) != 4 || q.Pop() != nil || len(q.lookup) != 0 { + t.Fatal("Wrong length") + } + + q.Done(f1) + q.Done(f2) + q.Done(f3) + q.Done(f4) + q.Done(f5) // Does not exist + + if len(q.progress) != 0 || q.Pop() != nil || len(q.lookup) != 0 { + t.Fatal("Wrong length") + } + + progress, queued = q.Jobs() + if len(progress) != 0 || len(queued) != 0 { + t.Fatal("Wrong length") + } + q.Bump("") + q.Done(f5) // Does not exist + progress, queued = q.Jobs() + if len(progress) != 0 || len(queued) != 0 { + t.Fatal("Wrong length") + } +} From 8f72ae9da2d9e7bbf76c2cbe6e168d2416477be3 Mon Sep 17 00:00:00 2001 From: Jakob Borg Date: Tue, 30 Dec 2014 09:07:49 +0100 Subject: [PATCH 3/7] Add some benchmarks --- internal/model/queue_test.go | 79 ++++++++++++++++++++++++++++++++++-- 1 file changed, 76 insertions(+), 3 deletions(-) diff --git a/internal/model/queue_test.go b/internal/model/queue_test.go index 00d5ffd6..b1171306 100644 --- a/internal/model/queue_test.go +++ b/internal/model/queue_test.go @@ -72,7 +72,7 @@ func TestJobQueue(t *testing.T) { } } - if len(q.progress) > 0 || len(q.lookup) != 4 || q.queued.Len() != 4 { + if len(q.progress) > 0 || q.queued.Len() != 4 { t.Fatal("Wrong length") } @@ -106,7 +106,7 @@ func TestJobQueue(t *testing.T) { } } - if len(q.progress) != 4 || q.Pop() != nil || len(q.lookup) != 0 { + if len(q.progress) != 4 || q.Pop() != nil { t.Fatal("Wrong length") } @@ -116,7 +116,7 @@ func TestJobQueue(t *testing.T) { q.Done(f4) q.Done(f5) // Does not exist - if len(q.progress) != 0 || q.Pop() != nil || len(q.lookup) != 0 { + if len(q.progress) != 0 || q.Pop() != nil { t.Fatal("Wrong length") } @@ -131,3 +131,76 @@ func TestJobQueue(t *testing.T) { t.Fatal("Wrong length") } } + +/* +func BenchmarkJobQueuePush(b *testing.B) { + files := genFiles(b.N) + + q := NewJobQueue() + + b.ResetTimer() + for i := 0; i < b.N; i++ { + q.Push(&files[i]) + } +} + +func BenchmarkJobQueuePop(b *testing.B) { + files := genFiles(b.N) + + q := NewJobQueue() + for j := range files { + q.Push(&files[j]) + } + + b.ResetTimer() + for i := 0; i < b.N; i++ { + q.Pop() + } +} + +func BenchmarkJobQueuePopDone(b *testing.B) { + files := genFiles(b.N) + + q := NewJobQueue() + for j := range files { + q.Push(&files[j]) + } + + b.ResetTimer() + for i := 0; i < b.N; i++ { + n := q.Pop() + q.Done(n) + } +} +*/ + +func BenchmarkJobQueueBump(b *testing.B) { + files := genFiles(b.N) + + q := NewJobQueue() + for j := range files { + q.Push(&files[j]) + } + + b.ResetTimer() + for i := 0; i < b.N; i++ { + q.Bump(files[i].Name) + } +} + +func BenchmarkJobQueuePushPopDone10k(b *testing.B) { + files := genFiles(10000) + + b.ResetTimer() + for i := 0; i < b.N; i++ { + q := NewJobQueue() + for j := range files { + q.Push(&files[j]) + } + for range files { + n := q.Pop() + q.Done(n) + } + } + +} From 34deb82aea27d25bb733285ae8c73786f0caf5dd Mon Sep 17 00:00:00 2001 From: Jakob Borg Date: Tue, 30 Dec 2014 09:07:58 +0100 Subject: [PATCH 4/7] Use slice instead of list, no map benchmark old ns/op new ns/op delta BenchmarkJobQueueBump 345 154498 +44682.03% BenchmarkJobQueuePushPopDone10k 9437373 3258204 -65.48% benchmark old allocs new allocs delta BenchmarkJobQueueBump 0 0 +0.00% BenchmarkJobQueuePushPopDone10k 10565 22 -99.79% benchmark old bytes new bytes delta BenchmarkJobQueueBump 0 0 +0.00% BenchmarkJobQueuePushPopDone10k 1452498 385869 -73.43% --- internal/model/queue.go | 43 ++++++++++++++---------------------- internal/model/queue_test.go | 2 +- 2 files changed, 18 insertions(+), 27 deletions(-) diff --git a/internal/model/queue.go b/internal/model/queue.go index fd4f4d55..0621a32d 100644 --- a/internal/model/queue.go +++ b/internal/model/queue.go @@ -16,7 +16,6 @@ package model import ( - "container/list" "sync" "github.com/syncthing/syncthing/internal/protocol" @@ -24,38 +23,31 @@ import ( type JobQueue struct { progress []*protocol.FileInfo - - queued *list.List - lookup map[string]*list.Element // O(1) lookups - - mut sync.Mutex + queued []*protocol.FileInfo + mut sync.Mutex } func NewJobQueue() *JobQueue { - return &JobQueue{ - progress: []*protocol.FileInfo{}, - queued: list.New(), - lookup: make(map[string]*list.Element), - } + return &JobQueue{} } func (q *JobQueue) Push(file *protocol.FileInfo) { q.mut.Lock() - defer q.mut.Unlock() - - q.lookup[file.Name] = q.queued.PushBack(file) + q.queued = append(q.queued, file) + q.mut.Unlock() } func (q *JobQueue) Pop() *protocol.FileInfo { q.mut.Lock() defer q.mut.Unlock() - if q.queued.Len() == 0 { + if len(q.queued) == 0 { return nil } - f := q.queued.Remove(q.queued.Front()).(*protocol.FileInfo) - delete(q.lookup, f.Name) + var f *protocol.FileInfo + f, q.queued[0] = q.queued[0], nil + q.queued = q.queued[1:] q.progress = append(q.progress, f) return f @@ -65,9 +57,11 @@ func (q *JobQueue) Bump(filename string) { q.mut.Lock() defer q.mut.Unlock() - ele, ok := q.lookup[filename] - if ok { - q.queued.MoveToFront(ele) + for i := range q.queued { + if q.queued[i].Name == filename { + q.queued[0], q.queued[i] = q.queued[i], q.queued[0] + return + } } } @@ -94,12 +88,9 @@ func (q *JobQueue) Jobs() ([]protocol.FileInfoTruncated, []protocol.FileInfoTrun progress[i] = q.progress[i].ToTruncated() } - queued := make([]protocol.FileInfoTruncated, q.queued.Len()) - i := 0 - for e := q.queued.Front(); e != nil; e = e.Next() { - fi := e.Value.(*protocol.FileInfo) - queued[i] = fi.ToTruncated() - i++ + queued := make([]protocol.FileInfoTruncated, len(q.queued)) + for i := range q.queued { + queued[i] = q.queued[i].ToTruncated() } return progress, queued diff --git a/internal/model/queue_test.go b/internal/model/queue_test.go index b1171306..16d6a57e 100644 --- a/internal/model/queue_test.go +++ b/internal/model/queue_test.go @@ -72,7 +72,7 @@ func TestJobQueue(t *testing.T) { } } - if len(q.progress) > 0 || q.queued.Len() != 4 { + if len(q.progress) > 0 || len(q.queued) != 4 { t.Fatal("Wrong length") } From 2496185629b2751bf5816b99246b9acd65d48b47 Mon Sep 17 00:00:00 2001 From: Jakob Borg Date: Tue, 30 Dec 2014 09:31:34 +0100 Subject: [PATCH 5/7] Only buffer file names, not full &FileInfo --- internal/model/model.go | 30 +++++++++++-------- internal/model/puller.go | 15 +++++----- internal/model/queue.go | 43 +++++++++++--------------- internal/model/queue_test.go | 58 ++++++++++++++++-------------------- internal/model/scanner.go | 4 +-- 5 files changed, 68 insertions(+), 82 deletions(-) diff --git a/internal/model/model.go b/internal/model/model.go index a1a3c770..4c3ed84a 100644 --- a/internal/model/model.go +++ b/internal/model/model.go @@ -79,7 +79,7 @@ const ( type service interface { Serve() Stop() - Jobs() ([]protocol.FileInfoTruncated, []protocol.FileInfoTruncated) // In progress, Queued + Jobs() ([]string, []string) // In progress, Queued Bump(string) } @@ -427,20 +427,25 @@ func (m *Model) NeedFolderFiles(folder string, max int) ([]protocol.FileInfoTrun m.fmut.RLock() defer m.fmut.RUnlock() if rf, ok := m.folderFiles[folder]; ok { - progress := []protocol.FileInfoTruncated{} - queued := []protocol.FileInfoTruncated{} - rest := []protocol.FileInfoTruncated{} - seen := map[string]struct{}{} + var progress, queued, rest []protocol.FileInfoTruncated + var seen map[string]bool runner, ok := m.folderRunners[folder] if ok { - progress, queued = runner.Jobs() - seen = make(map[string]struct{}, len(progress)+len(queued)) - for _, file := range progress { - seen[file.Name] = struct{}{} + progressNames, queuedNames := runner.Jobs() + + progress = make([]protocol.FileInfoTruncated, len(progressNames)) + queued = make([]protocol.FileInfoTruncated, len(queuedNames)) + seen = make(map[string]bool, len(progressNames)+len(queuedNames)) + + for i, name := range progressNames { + progress[i] = rf.GetGlobal(name).ToTruncated() /// XXX: Should implement GetGlobalTruncated directly + seen[name] = true } - for _, file := range queued { - seen[file.Name] = struct{}{} + + for i, name := range queuedNames { + queued[i] = rf.GetGlobal(name).ToTruncated() /// XXX: Should implement GetGlobalTruncated directly + seen[name] = true } } left := max - len(progress) - len(queued) @@ -448,8 +453,7 @@ func (m *Model) NeedFolderFiles(folder string, max int) ([]protocol.FileInfoTrun rf.WithNeedTruncated(protocol.LocalDeviceID, func(f protocol.FileIntf) bool { left-- ft := f.(protocol.FileInfoTruncated) - _, ok := seen[ft.Name] - if !ok { + if !seen[ft.Name] { rest = append(rest, ft) } return max < 1 || left > 0 diff --git a/internal/model/puller.go b/internal/model/puller.go index e908dfa5..5468a7ae 100644 --- a/internal/model/puller.go +++ b/internal/model/puller.go @@ -340,7 +340,7 @@ func (p *Puller) pullerIteration(checksum bool, ignores *ignore.Matcher) int { default: // A new or changed file or symlink. This is the only case where we // do stuff concurrently in the background - p.queue.Push(&file) + p.queue.Push(file.Name) } changed++ @@ -348,11 +348,12 @@ func (p *Puller) pullerIteration(checksum bool, ignores *ignore.Matcher) int { }) for { - f := p.queue.Pop() - if f == nil { + fileName, ok := p.queue.Pop() + if !ok { break } - p.handleFile(*f, copyChan, finisherChan) + f := p.model.CurrentGlobalFile(p.folder, fileName) + p.handleFile(f, copyChan, finisherChan) } // Signal copy and puller routines that we are done with the in data for @@ -492,7 +493,7 @@ func (p *Puller) handleFile(file protocol.FileInfo, copyChan chan<- copyBlocksSt if debug { l.Debugln(p, "taking shortcut on", file.Name) } - p.queue.Done(&file) + p.queue.Done(file.Name) if file.IsSymlink() { p.shortcutSymlink(curFile, file) } else { @@ -860,7 +861,7 @@ func (p *Puller) finisherRoutine(in <-chan *sharedPullerState) { continue } - p.queue.Done(&state.file) + p.queue.Done(state.file.Name) p.performFinish(state) p.model.receivedFile(p.folder, state.file.Name) if p.progressEmitter != nil { @@ -875,7 +876,7 @@ func (p *Puller) Bump(filename string) { p.queue.Bump(filename) } -func (p *Puller) Jobs() ([]protocol.FileInfoTruncated, []protocol.FileInfoTruncated) { +func (p *Puller) Jobs() ([]string, []string) { return p.queue.Jobs() } diff --git a/internal/model/queue.go b/internal/model/queue.go index 0621a32d..24f10170 100644 --- a/internal/model/queue.go +++ b/internal/model/queue.go @@ -15,15 +15,11 @@ package model -import ( - "sync" - - "github.com/syncthing/syncthing/internal/protocol" -) +import "sync" type JobQueue struct { - progress []*protocol.FileInfo - queued []*protocol.FileInfo + progress []string + queued []string mut sync.Mutex } @@ -31,26 +27,26 @@ func NewJobQueue() *JobQueue { return &JobQueue{} } -func (q *JobQueue) Push(file *protocol.FileInfo) { +func (q *JobQueue) Push(file string) { q.mut.Lock() q.queued = append(q.queued, file) q.mut.Unlock() } -func (q *JobQueue) Pop() *protocol.FileInfo { +func (q *JobQueue) Pop() (string, bool) { q.mut.Lock() defer q.mut.Unlock() if len(q.queued) == 0 { - return nil + return "", false } - var f *protocol.FileInfo - f, q.queued[0] = q.queued[0], nil + var f string + f = q.queued[0] q.queued = q.queued[1:] q.progress = append(q.progress, f) - return f + return f, true } func (q *JobQueue) Bump(filename string) { @@ -58,40 +54,35 @@ func (q *JobQueue) Bump(filename string) { defer q.mut.Unlock() for i := range q.queued { - if q.queued[i].Name == filename { + if q.queued[i] == filename { q.queued[0], q.queued[i] = q.queued[i], q.queued[0] return } } } -func (q *JobQueue) Done(file *protocol.FileInfo) { +func (q *JobQueue) Done(file string) { q.mut.Lock() defer q.mut.Unlock() for i := range q.progress { - if q.progress[i].Name == file.Name { + if q.progress[i] == file { copy(q.progress[i:], q.progress[i+1:]) - q.progress[len(q.progress)-1] = nil q.progress = q.progress[:len(q.progress)-1] return } } } -func (q *JobQueue) Jobs() ([]protocol.FileInfoTruncated, []protocol.FileInfoTruncated) { +func (q *JobQueue) Jobs() ([]string, []string) { q.mut.Lock() defer q.mut.Unlock() - progress := make([]protocol.FileInfoTruncated, len(q.progress)) - for i := range q.progress { - progress[i] = q.progress[i].ToTruncated() - } + progress := make([]string, len(q.progress)) + copy(progress, q.progress) - queued := make([]protocol.FileInfoTruncated, len(q.queued)) - for i := range q.queued { - queued[i] = q.queued[i].ToTruncated() - } + queued := make([]string, len(q.queued)) + copy(queued, q.queued) return progress, queued } diff --git a/internal/model/queue_test.go b/internal/model/queue_test.go index 16d6a57e..cfe8be70 100644 --- a/internal/model/queue_test.go +++ b/internal/model/queue_test.go @@ -18,25 +18,15 @@ package model import ( "fmt" "testing" - - "github.com/syncthing/syncthing/internal/protocol" -) - -var ( - f1 = &protocol.FileInfo{Name: "f1"} - f2 = &protocol.FileInfo{Name: "f2"} - f3 = &protocol.FileInfo{Name: "f3"} - f4 = &protocol.FileInfo{Name: "f4"} - f5 = &protocol.FileInfo{Name: "f5"} ) func TestJobQueue(t *testing.T) { // Some random actions q := NewJobQueue() - q.Push(f1) - q.Push(f2) - q.Push(f3) - q.Push(f4) + q.Push("f1") + q.Push("f2") + q.Push("f3") + q.Push("f4") progress, queued := q.Jobs() if len(progress) != 0 || len(queued) != 4 { @@ -44,8 +34,8 @@ func TestJobQueue(t *testing.T) { } for i := 1; i < 5; i++ { - n := q.Pop() - if n == nil || n.Name != fmt.Sprintf("f%d", i) { + n, ok := q.Pop() + if !ok || n != fmt.Sprintf("f%d", i) { t.Fatal("Wrong element") } progress, queued = q.Jobs() @@ -65,7 +55,7 @@ func TestJobQueue(t *testing.T) { t.Fatal("Wrong length") } - q.Done(f5) // Does not exist + q.Done("f5") // Does not exist progress, queued = q.Jobs() if len(progress) != 0 || len(queued) != 4 { t.Fatal("Wrong length") @@ -90,8 +80,8 @@ func TestJobQueue(t *testing.T) { t.Fatal("Wrong length") } - n := q.Pop() - if n == nil || n.Name != s { + n, ok := q.Pop() + if !ok || n != s { t.Fatal("Wrong element") } progress, queued = q.Jobs() @@ -99,24 +89,26 @@ func TestJobQueue(t *testing.T) { t.Fatal("Wrong length") } - q.Done(f5) // Does not exist + q.Done("f5") // Does not exist progress, queued = q.Jobs() if len(progress) != 5-i || len(queued) != i-1 { t.Fatal("Wrong length") } } - if len(q.progress) != 4 || q.Pop() != nil { + _, ok := q.Pop() + if len(q.progress) != 4 || ok { t.Fatal("Wrong length") } - q.Done(f1) - q.Done(f2) - q.Done(f3) - q.Done(f4) - q.Done(f5) // Does not exist + q.Done("f1") + q.Done("f2") + q.Done("f3") + q.Done("f4") + q.Done("f5") // Does not exist - if len(q.progress) != 0 || q.Pop() != nil { + _, ok = q.Pop() + if len(q.progress) != 0 || ok { t.Fatal("Wrong length") } @@ -125,7 +117,7 @@ func TestJobQueue(t *testing.T) { t.Fatal("Wrong length") } q.Bump("") - q.Done(f5) // Does not exist + q.Done("f5") // Does not exist progress, queued = q.Jobs() if len(progress) != 0 || len(queued) != 0 { t.Fatal("Wrong length") @@ -178,8 +170,8 @@ func BenchmarkJobQueueBump(b *testing.B) { files := genFiles(b.N) q := NewJobQueue() - for j := range files { - q.Push(&files[j]) + for _, f := range files { + q.Push(f.Name) } b.ResetTimer() @@ -194,11 +186,11 @@ func BenchmarkJobQueuePushPopDone10k(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { q := NewJobQueue() - for j := range files { - q.Push(&files[j]) + for _, f := range files { + q.Push(f.Name) } for range files { - n := q.Pop() + n, _ := q.Pop() q.Done(n) } } diff --git a/internal/model/scanner.go b/internal/model/scanner.go index 6290aef3..12b4853c 100644 --- a/internal/model/scanner.go +++ b/internal/model/scanner.go @@ -18,8 +18,6 @@ package model import ( "fmt" "time" - - "github.com/syncthing/syncthing/internal/protocol" ) type Scanner struct { @@ -80,6 +78,6 @@ func (s *Scanner) String() string { func (s *Scanner) Bump(string) {} -func (s *Scanner) Jobs() ([]protocol.FileInfoTruncated, []protocol.FileInfoTruncated) { +func (s *Scanner) Jobs() ([]string, []string) { return nil, nil } From 5143c09bcf344db88432f666943f4ecf7f8ae00d Mon Sep 17 00:00:00 2001 From: Jakob Borg Date: Tue, 30 Dec 2014 09:35:21 +0100 Subject: [PATCH 6/7] Refactor / cleanup --- cmd/syncthing/gui.go | 2 +- internal/model/model.go | 7 +-- internal/model/puller.go | 24 ++-------- internal/model/queue.go | 16 +++---- internal/model/queue_test.go | 88 ++++++++++++++++++------------------ internal/model/scanner.go | 2 +- 6 files changed, 62 insertions(+), 77 deletions(-) diff --git a/cmd/syncthing/gui.go b/cmd/syncthing/gui.go index b0f5ac4c..cbf2da76 100644 --- a/cmd/syncthing/gui.go +++ b/cmd/syncthing/gui.go @@ -648,7 +648,7 @@ func restPostBump(m *model.Model, w http.ResponseWriter, r *http.Request) { qs := r.URL.Query() folder := qs.Get("folder") file := qs.Get("file") - m.Bump(folder, file) + m.BringToFront(folder, file) restGetNeed(m, w, r) } diff --git a/internal/model/model.go b/internal/model/model.go index 4c3ed84a..c17bc366 100644 --- a/internal/model/model.go +++ b/internal/model/model.go @@ -80,7 +80,7 @@ type service interface { Serve() Stop() Jobs() ([]string, []string) // In progress, Queued - Bump(string) + BringToFront(string) } type Model struct { @@ -191,6 +191,7 @@ func (m *Model) StartFolderRW(folder string) { copiers: cfg.Copiers, pullers: cfg.Pullers, finishers: cfg.Finishers, + queue: newJobQueue(), } m.folderRunners[folder] = p m.fmut.Unlock() @@ -1390,13 +1391,13 @@ func (m *Model) availability(folder, file string) []protocol.DeviceID { } // Bump the given files priority in the job queue -func (m *Model) Bump(folder, file string) { +func (m *Model) BringToFront(folder, file string) { m.pmut.RLock() defer m.pmut.RUnlock() runner, ok := m.folderRunners[folder] if ok { - runner.Bump(file) + runner.BringToFront(file) } } diff --git a/internal/model/puller.go b/internal/model/puller.go index 5468a7ae..e1d063dd 100644 --- a/internal/model/puller.go +++ b/internal/model/puller.go @@ -78,7 +78,7 @@ type Puller struct { copiers int pullers int finishers int - queue *JobQueue + queue *jobQueue } // Serve will run scans and pulls. It will return when Stop()ed or on a @@ -90,7 +90,6 @@ func (p *Puller) Serve() { } p.stop = make(chan struct{}) - p.queue = NewJobQueue() pullTimer := time.NewTimer(checkPullIntv) scanTimer := time.NewTimer(time.Millisecond) // The first scan should be done immediately. @@ -872,31 +871,14 @@ func (p *Puller) finisherRoutine(in <-chan *sharedPullerState) { } // Moves the given filename to the front of the job queue -func (p *Puller) Bump(filename string) { - p.queue.Bump(filename) +func (p *Puller) BringToFront(filename string) { + p.queue.BringToFront(filename) } func (p *Puller) Jobs() ([]string, []string) { return p.queue.Jobs() } -// clean deletes orphaned temporary files -func (p *Puller) clean() { - keep := time.Duration(p.model.cfg.Options().KeepTemporariesH) * time.Hour - now := time.Now() - filepath.Walk(p.dir, func(path string, info os.FileInfo, err error) error { - if err != nil { - return err - } - - if info.Mode().IsRegular() && defTempNamer.IsTemporary(path) && info.ModTime().Add(keep).Before(now) { - os.Remove(path) - } - - return nil - }) -} - func invalidateFolder(cfg *config.Configuration, folderID string, err error) { for i := range cfg.Folders { folder := &cfg.Folders[i] diff --git a/internal/model/queue.go b/internal/model/queue.go index 24f10170..b2d7b1f8 100644 --- a/internal/model/queue.go +++ b/internal/model/queue.go @@ -17,23 +17,23 @@ package model import "sync" -type JobQueue struct { +type jobQueue struct { progress []string queued []string mut sync.Mutex } -func NewJobQueue() *JobQueue { - return &JobQueue{} +func newJobQueue() *jobQueue { + return &jobQueue{} } -func (q *JobQueue) Push(file string) { +func (q *jobQueue) Push(file string) { q.mut.Lock() q.queued = append(q.queued, file) q.mut.Unlock() } -func (q *JobQueue) Pop() (string, bool) { +func (q *jobQueue) Pop() (string, bool) { q.mut.Lock() defer q.mut.Unlock() @@ -49,7 +49,7 @@ func (q *JobQueue) Pop() (string, bool) { return f, true } -func (q *JobQueue) Bump(filename string) { +func (q *jobQueue) BringToFront(filename string) { q.mut.Lock() defer q.mut.Unlock() @@ -61,7 +61,7 @@ func (q *JobQueue) Bump(filename string) { } } -func (q *JobQueue) Done(file string) { +func (q *jobQueue) Done(file string) { q.mut.Lock() defer q.mut.Unlock() @@ -74,7 +74,7 @@ func (q *JobQueue) Done(file string) { } } -func (q *JobQueue) Jobs() ([]string, []string) { +func (q *jobQueue) Jobs() ([]string, []string) { q.mut.Lock() defer q.mut.Unlock() diff --git a/internal/model/queue_test.go b/internal/model/queue_test.go index cfe8be70..37456644 100644 --- a/internal/model/queue_test.go +++ b/internal/model/queue_test.go @@ -17,12 +17,13 @@ package model import ( "fmt" + "reflect" "testing" ) func TestJobQueue(t *testing.T) { // Some random actions - q := NewJobQueue() + q := newJobQueue() q.Push("f1") q.Push("f2") q.Push("f3") @@ -40,6 +41,8 @@ func TestJobQueue(t *testing.T) { } progress, queued = q.Jobs() if len(progress) != 1 || len(queued) != 3 { + t.Log(progress) + t.Log(queued) t.Fatal("Wrong length") } @@ -74,7 +77,7 @@ func TestJobQueue(t *testing.T) { s := fmt.Sprintf("f%d", i) - q.Bump(s) + q.BringToFront(s) progress, queued = q.Jobs() if len(progress) != 4-i || len(queued) != i { t.Fatal("Wrong length") @@ -116,7 +119,7 @@ func TestJobQueue(t *testing.T) { if len(progress) != 0 || len(queued) != 0 { t.Fatal("Wrong length") } - q.Bump("") + q.BringToFront("") q.Done("f5") // Does not exist progress, queued = q.Jobs() if len(progress) != 0 || len(queued) != 0 { @@ -124,59 +127,58 @@ func TestJobQueue(t *testing.T) { } } -/* -func BenchmarkJobQueuePush(b *testing.B) { - files := genFiles(b.N) +func TestBringToFront(t *testing.T) { + q := newJobQueue() + q.Push("f1") + q.Push("f2") + q.Push("f3") + q.Push("f4") - q := NewJobQueue() + _, queued := q.Jobs() + if !reflect.DeepEqual(queued, []string{"f1", "f2", "f3", "f4"}) { + t.Errorf("Incorrect order %v at start", queued) + } - b.ResetTimer() - for i := 0; i < b.N; i++ { - q.Push(&files[i]) + q.BringToFront("f1") // corner case: does nothing + + _, queued = q.Jobs() + if !reflect.DeepEqual(queued, []string{"f1", "f2", "f3", "f4"}) { + t.Errorf("Incorrect order %v", queued) + } + + q.BringToFront("f3") + + _, queued = q.Jobs() + if !reflect.DeepEqual(queued, []string{"f3", "f1", "f2", "f4"}) { + t.Errorf("Incorrect order %v", queued) + } + + q.BringToFront("f2") + + _, queued = q.Jobs() + if !reflect.DeepEqual(queued, []string{"f2", "f3", "f1", "f4"}) { + t.Errorf("Incorrect order %v", queued) + } + + q.BringToFront("f4") // corner case: last element + + _, queued = q.Jobs() + if !reflect.DeepEqual(queued, []string{"f4", "f2", "f3", "f1"}) { + t.Errorf("Incorrect order %v", queued) } } -func BenchmarkJobQueuePop(b *testing.B) { - files := genFiles(b.N) - - q := NewJobQueue() - for j := range files { - q.Push(&files[j]) - } - - b.ResetTimer() - for i := 0; i < b.N; i++ { - q.Pop() - } -} - -func BenchmarkJobQueuePopDone(b *testing.B) { - files := genFiles(b.N) - - q := NewJobQueue() - for j := range files { - q.Push(&files[j]) - } - - b.ResetTimer() - for i := 0; i < b.N; i++ { - n := q.Pop() - q.Done(n) - } -} -*/ - func BenchmarkJobQueueBump(b *testing.B) { files := genFiles(b.N) - q := NewJobQueue() + q := newJobQueue() for _, f := range files { q.Push(f.Name) } b.ResetTimer() for i := 0; i < b.N; i++ { - q.Bump(files[i].Name) + q.BringToFront(files[i].Name) } } @@ -185,7 +187,7 @@ func BenchmarkJobQueuePushPopDone10k(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { - q := NewJobQueue() + q := newJobQueue() for _, f := range files { q.Push(f.Name) } diff --git a/internal/model/scanner.go b/internal/model/scanner.go index 12b4853c..c2824b60 100644 --- a/internal/model/scanner.go +++ b/internal/model/scanner.go @@ -76,7 +76,7 @@ func (s *Scanner) String() string { return fmt.Sprintf("scanner/%s@%p", s.folder, s) } -func (s *Scanner) Bump(string) {} +func (s *Scanner) BringToFront(string) {} func (s *Scanner) Jobs() ([]string, []string) { return nil, nil From 9b5e8aaf8386106ccd74d6309dc1abb3427ae76c Mon Sep 17 00:00:00 2001 From: Jakob Borg Date: Fri, 2 Jan 2015 15:45:59 +0100 Subject: [PATCH 7/7] Repair buggy BringToFront --- internal/model/queue.go | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/internal/model/queue.go b/internal/model/queue.go index b2d7b1f8..a4f34dab 100644 --- a/internal/model/queue.go +++ b/internal/model/queue.go @@ -53,9 +53,15 @@ func (q *jobQueue) BringToFront(filename string) { q.mut.Lock() defer q.mut.Unlock() - for i := range q.queued { - if q.queued[i] == filename { - q.queued[0], q.queued[i] = q.queued[i], q.queued[0] + for i, cur := range q.queued { + if cur == filename { + if i > 0 { + // Shift the elements before the selected element one step to + // the right, overwriting the selected element + copy(q.queued[1:i+1], q.queued[0:]) + // Put the selected element at the front + q.queued[0] = cur + } return } }