Bug 345852 - Save personal dictionary when a word is added or removed. r=ehsan
authorJorg K <mozilla@jorgk.com>
Wed, 22 Jul 2015 02:06:00 -0400
changeset 254186 48df0e84304980714996e5fe9f1e5df65e3630a5
parent 254185 1e8f06cd9655947f23eb55396f096a7b72bd9bf1
child 254187 1b480d62f2f04ead8fe0e3935c648f35ac42e7ea
push id29094
push usercbook@mozilla.com
push dateThu, 23 Jul 2015 10:02:48 +0000
treeherdermozilla-central@ea320711a2e3 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersehsan
bugs345852
milestone42.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 345852 - Save personal dictionary when a word is added or removed. r=ehsan
extensions/spellcheck/src/mozPersonalDictionary.cpp
extensions/spellcheck/src/mozPersonalDictionary.h
--- a/extensions/spellcheck/src/mozPersonalDictionary.cpp
+++ b/extensions/spellcheck/src/mozPersonalDictionary.cpp
@@ -20,16 +20,17 @@
 #include "nsISafeOutputStream.h"
 #include "nsTArray.h"
 #include "nsStringEnumerator.h"
 #include "nsUnicharInputStream.h"
 #include "nsIRunnable.h"
 #include "nsThreadUtils.h"
 #include "nsProxyRelease.h"
 #include "prio.h"
+#include "mozilla/Move.h"
 
 #define MOZ_PERSONAL_DICT_NAME "persdict.dat"
 
 /**
  * This is the most braindead implementation of a personal dictionary possible.
  * There is not much complexity needed, though.  It could be made much faster,
  *  and probably should, but I don't see much need for more in terms of interface.
  *
@@ -54,17 +55,17 @@ NS_IMPL_CYCLE_COLLECTION(mozPersonalDict
 
 class mozPersonalDictionaryLoader final : public nsRunnable
 {
 public:
   explicit mozPersonalDictionaryLoader(mozPersonalDictionary *dict) : mDict(dict)
   {
   }
 
-  NS_IMETHOD Run()
+  NS_IMETHOD Run() override
   {
     mDict->SyncLoad();
 
     // Release the dictionary on the main thread
     mozPersonalDictionary *dict;
     mDict.forget(&dict);
 
     nsCOMPtr<nsIThread> mainThread = do_GetMainThread();
@@ -77,20 +78,102 @@ public:
 
     return NS_OK;
   }
 
 private:
   nsRefPtr<mozPersonalDictionary> mDict;
 };
 
+class mozPersonalDictionarySave final : public nsRunnable
+{
+public:
+  explicit mozPersonalDictionarySave(mozPersonalDictionary *aDict,
+                                     nsCOMPtr<nsIFile> aFile,
+                                     nsTArray<nsString> &&aDictWords)
+    : mDictWords(aDictWords),
+      mFile(aFile),
+      mDict(aDict)
+  {
+  }
+
+  NS_IMETHOD Run() override
+  {
+    nsresult res;
+
+    MOZ_ASSERT(!NS_IsMainThread());
+
+    {
+      mozilla::MonitorAutoLock mon(mDict->mMonitorSave);
+
+      nsCOMPtr<nsIOutputStream> outStream;
+      NS_NewSafeLocalFileOutputStream(getter_AddRefs(outStream), mFile,
+                                      PR_CREATE_FILE | PR_WRONLY | PR_TRUNCATE,
+                                      0664);
+
+      // Get a buffered output stream 4096 bytes big, to optimize writes.
+      nsCOMPtr<nsIOutputStream> bufferedOutputStream;
+      res = NS_NewBufferedOutputStream(getter_AddRefs(bufferedOutputStream),
+                                       outStream, 4096);
+      if (NS_FAILED(res)) {
+        return res;
+      }
+
+      uint32_t bytesWritten;
+      nsAutoCString utf8Key;
+      for (uint32_t i = 0; i < mDictWords.Length(); ++i) {
+        CopyUTF16toUTF8(mDictWords[i], utf8Key);
+
+        bufferedOutputStream->Write(utf8Key.get(), utf8Key.Length(),
+                                    &bytesWritten);
+        bufferedOutputStream->Write("\n", 1, &bytesWritten);
+      }
+      nsCOMPtr<nsISafeOutputStream> safeStream =
+        do_QueryInterface(bufferedOutputStream);
+      NS_ASSERTION(safeStream, "expected a safe output stream!");
+      if (safeStream) {
+        res = safeStream->Finish();
+        if (NS_FAILED(res)) {
+          NS_WARNING("failed to save personal dictionary file! possible data loss");
+        }
+      }
+
+      // Save is done, reset the state variable and notify those who are waiting.
+      mDict->mSavePending = false;
+      mon.Notify();
+
+      // Leaving the block where 'mon' was declared will call the destructor
+      // and unlock.
+    }
+
+    // Release the dictionary on the main thread.
+    mozPersonalDictionary *dict;
+    mDict.forget(&dict);
+
+    nsCOMPtr<nsIThread> mainThread = do_GetMainThread();
+    if (mainThread) {
+      NS_ProxyRelease(mainThread, static_cast<mozIPersonalDictionary *>(dict));
+    } else {
+      // It's better to leak the dictionary than to release it on a wrong thread.
+      NS_WARNING("Cannot get main thread, leaking mozPersonalDictionary.");
+    }
+    return NS_OK;
+  }
+
+private:
+  nsTArray<nsString> mDictWords;
+  nsCOMPtr<nsIFile> mFile;
+  nsRefPtr<mozPersonalDictionary> mDict;
+};
+
 mozPersonalDictionary::mozPersonalDictionary()
- : mDirty(false),
-   mIsLoaded(false),
-   mMonitor("mozPersonalDictionary::mMonitor")
+ : mIsLoaded(false),
+   mSavePending(false),
+   mMonitor("mozPersonalDictionary::mMonitor"),
+   mMonitorSave("mozPersonalDictionary::mMonitorSave")
 {
 }
 
 mozPersonalDictionary::~mozPersonalDictionary()
 {
 }
 
 nsresult mozPersonalDictionary::Init()
@@ -112,22 +195,30 @@ nsresult mozPersonalDictionary::Init()
 
   Load();
 
   return NS_OK;
 }
 
 void mozPersonalDictionary::WaitForLoad()
 {
+  // If the dictionary is already loaded, we return straight away.
   if (mIsLoaded) {
     return;
   }
 
+  // If the dictionary hasn't been loaded, we try to lock the same monitor
+  // that the thread uses that does the load. This way the main thread will 
+  // be suspended until the monitor becomes available.
   mozilla::MonitorAutoLock mon(mMonitor);
 
+  // The monitor has become available. This can have two reasons:
+  // 1: The thread that does the load has finished.
+  // 2: The thread that does the load hasn't even started.
+  //    In this case we need to wait.
   if (!mIsLoaded) {
     mon.Wait();
   }
 }
 
 nsresult mozPersonalDictionary::LoadInternal()
 {
   nsresult rv;
@@ -234,63 +325,71 @@ void mozPersonalDictionary::SyncLoadInte
       nsAutoString word;
       while((c != '\n') && (c != '\r') && !done){
         word.Append(c);
         if( (NS_OK != convStream->Read(&c, 1, &nRead)) || (nRead != 1)) done = true;
       }
       mDictionaryTable.PutEntry(word.get());
     }
   } while(!done);
-  mDirty = false;
 }
 
-/* void Save (); */
+void mozPersonalDictionary::WaitForSave()
+{
+  // If no save is pending, we return straight away.
+  if (!mSavePending) {
+    return;
+  }
+
+  // If a save is pending, we try to lock the same monitor that the thread uses
+  // that does the save. This way the main thread will be suspended until the
+  // monitor becomes available.
+  mozilla::MonitorAutoLock mon(mMonitorSave);
+
+  // The monitor has become available. This can have two reasons:
+  // 1: The thread that does the save has finished.
+  // 2: The thread that does the save hasn't even started.
+  //    In this case we need to wait.
+  if (mSavePending) {
+    mon.Wait();
+  }
+}
+
 NS_IMETHODIMP mozPersonalDictionary::Save()
 {
   nsCOMPtr<nsIFile> theFile;
   nsresult res;
 
-  WaitForLoad();
-  if(!mDirty) return NS_OK;
+  WaitForSave();
+
+  mSavePending = true;
 
   //FIXME Deinst  -- get dictionary name from prefs;
   res = NS_GetSpecialDirectory(NS_APP_USER_PROFILE_50_DIR, getter_AddRefs(theFile));
   if(NS_FAILED(res)) return res;
   if(!theFile)return NS_ERROR_FAILURE;
   res = theFile->Append(NS_LITERAL_STRING(MOZ_PERSONAL_DICT_NAME));
   if(NS_FAILED(res)) return res;
 
-  nsCOMPtr<nsIOutputStream> outStream;
-  NS_NewSafeLocalFileOutputStream(getter_AddRefs(outStream), theFile, PR_CREATE_FILE | PR_WRONLY | PR_TRUNCATE ,0664);
-
-  // get a buffered output stream 4096 bytes big, to optimize writes
-  nsCOMPtr<nsIOutputStream> bufferedOutputStream;
-  res = NS_NewBufferedOutputStream(getter_AddRefs(bufferedOutputStream), outStream, 4096);
-  if (NS_FAILED(res)) return res;
+  nsCOMPtr<nsIEventTarget> target =
+    do_GetService(NS_STREAMTRANSPORTSERVICE_CONTRACTID, &res);
+  if (NS_WARN_IF(NS_FAILED(res))) {
+    return res;
+  }
 
   nsTArray<nsString> array(mDictionaryTable.Count());
   for (auto iter = mDictionaryTable.Iter(); !iter.Done(); iter.Next()) {
     array.AppendElement(nsDependentString(iter.Get()->GetKey()));
   }
 
-  uint32_t bytesWritten;
-  nsAutoCString utf8Key;
-  for (uint32_t i = 0; i < array.Length(); ++i ) {
-    CopyUTF16toUTF8(array[i], utf8Key);
-
-    bufferedOutputStream->Write(utf8Key.get(), utf8Key.Length(), &bytesWritten);
-    bufferedOutputStream->Write("\n", 1, &bytesWritten);
-  }
-  nsCOMPtr<nsISafeOutputStream> safeStream = do_QueryInterface(bufferedOutputStream);
-  NS_ASSERTION(safeStream, "expected a safe output stream!");
-  if (safeStream) {
-    res = safeStream->Finish();
-    if (NS_FAILED(res)) {
-      NS_WARNING("failed to save personal dictionary file! possible data loss");
-    }
+  nsCOMPtr<nsIRunnable> runnable =
+    new mozPersonalDictionarySave(this, theFile, mozilla::Move(array));
+  res = target->Dispatch(runnable, NS_DISPATCH_NORMAL);
+  if (NS_WARN_IF(NS_FAILED(res))) {
+    return res;
   }
   return res;
 }
 
 /* readonly attribute nsIStringEnumerator GetWordList() */
 NS_IMETHODIMP mozPersonalDictionary::GetWordList(nsIStringEnumerator **aWords)
 {
   NS_ENSURE_ARG_POINTER(aWords);
@@ -318,48 +417,50 @@ NS_IMETHODIMP mozPersonalDictionary::Che
 
   *aResult = (mDictionaryTable.GetEntry(aWord) || mIgnoreTable.GetEntry(aWord));
   return NS_OK;
 }
 
 /* void AddWord (in wstring word); */
 NS_IMETHODIMP mozPersonalDictionary::AddWord(const char16_t *aWord, const char16_t *aLang)
 {
+  nsresult res;
   WaitForLoad();
 
   mDictionaryTable.PutEntry(aWord);
-  mDirty = true;
-  return NS_OK;
+  res = Save();
+  return res;
 }
 
 /* void RemoveWord (in wstring word); */
 NS_IMETHODIMP mozPersonalDictionary::RemoveWord(const char16_t *aWord, const char16_t *aLang)
 {
+  nsresult res;
   WaitForLoad();
 
   mDictionaryTable.RemoveEntry(aWord);
-  mDirty = true;
-  return NS_OK;
+  res = Save();
+  return res;
 }
 
 /* void IgnoreWord (in wstring word); */
 NS_IMETHODIMP mozPersonalDictionary::IgnoreWord(const char16_t *aWord)
 {
   // avoid adding duplicate words to the ignore list
   if (aWord && !mIgnoreTable.GetEntry(aWord)) 
     mIgnoreTable.PutEntry(aWord);
   return NS_OK;
 }
 
 /* void EndSession (); */
 NS_IMETHODIMP mozPersonalDictionary::EndSession()
 {
   WaitForLoad();
 
-  Save(); // save any custom words at the end of a spell check session
+  WaitForSave();
   mIgnoreTable.Clear();
   return NS_OK;
 }
 
 /* void AddCorrection (in wstring word, in wstring correction); */
 NS_IMETHODIMP mozPersonalDictionary::AddCorrection(const char16_t *word, const char16_t *correction, const char16_t *lang)
 {
     return NS_ERROR_NOT_IMPLEMENTED;
@@ -382,13 +483,13 @@ NS_IMETHODIMP mozPersonalDictionary::Obs
 {
   if (!nsCRT::strcmp(aTopic, "profile-do-change")) {
     // The observer is registered in Init() which calls Load and in turn
     // LoadInternal(); i.e. Observe() can't be called before Load().
     WaitForLoad();
     mIsLoaded = false;
     Load(); // load automatically clears out the existing dictionary table
   } else if (!nsCRT::strcmp(aTopic, "profile-before-change")) {
-    Save();
+    WaitForSave();
   }
 
   return NS_OK;
 }
--- a/extensions/spellcheck/src/mozPersonalDictionary.h
+++ b/extensions/spellcheck/src/mozPersonalDictionary.h
@@ -20,16 +20,17 @@
 
 #define MOZ_PERSONALDICTIONARY_CONTRACTID "@mozilla.org/spellchecker/personaldictionary;1"
 #define MOZ_PERSONALDICTIONARY_CID         \
 { /* 7EF52EAF-B7E1-462B-87E2-5D1DBACA9048 */  \
 0X7EF52EAF, 0XB7E1, 0X462B, \
   { 0X87, 0XE2, 0X5D, 0X1D, 0XBA, 0XCA, 0X90, 0X48 } }
 
 class mozPersonalDictionaryLoader;
+class mozPersonalDictionarySave;
 
 class mozPersonalDictionary final : public mozIPersonalDictionary,
                                     public nsIObserver,
                                     public nsSupportsWeakReference
 {
 public:
   NS_DECL_CYCLE_COLLECTING_ISUPPORTS
   NS_DECL_MOZIPERSONALDICTIONARY
@@ -38,24 +39,25 @@ public:
 
   mozPersonalDictionary();
 
   nsresult Init();
 
 protected:
   virtual ~mozPersonalDictionary();
 
-  /* has the dictionary been modified */
-  bool mDirty;
-
   /* true if the dictionary has been loaded from disk */
   bool mIsLoaded;
 
+  /* true if a dictionary save is pending */
+  bool mSavePending;
+
   nsCOMPtr<nsIFile> mFile;
   mozilla::Monitor mMonitor;
+  mozilla::Monitor mMonitorSave;
   nsTHashtable<nsUnicharPtrHashKey> mDictionaryTable;
   nsTHashtable<nsUnicharPtrHashKey> mIgnoreTable;
 
   /*Encoder to use to compare with spellchecker word */
   nsCOMPtr<nsIUnicodeEncoder>  mEncoder;
 
 private:
   /* wait for the asynchronous load of the dictionary to be completed */
@@ -66,12 +68,16 @@ private:
 
   /* launch an asynchrounous load of the dictionary from the main-thread
    * after successfully initializing mFile with the path of the dictionary */
   nsresult LoadInternal();
 
   /* perform a synchronous load of the dictionary from disk */
   void SyncLoadInternal();
 
+  /* wait for the asynchronous save of the dictionary to be completed */
+  void WaitForSave();
+
   friend class mozPersonalDictionaryLoader;
+  friend class mozPersonalDictionarySave;
 };
 
 #endif