AUP3: Many corrections and (safe) speed ups

This reenables synchronous mode by default.  However, for processing
where a power cut or crash can be tolerated, synchronous and journaling
are disabled.  Once the processing is complete, they are reenabled.

Types of processing that are like this are "Save As", "Backup Project",
and "Vacuuming". They all write to a separate project file while
running, so the real project file is safe.

Unfortunately, effects are back to be slow and sluggish.

I believe I've address all of the weird file corruption issues and
I'll continue to continue testing for these.
This commit is contained in:
Leland Lucius 2020-07-17 16:17:42 -05:00
parent c0ec20a641
commit 93d9303c3d
5 changed files with 345 additions and 242 deletions

View File

@ -37,6 +37,7 @@ list( APPEND DEFINES
#
SQLITE_DQS=0
SQLITE_DEFAULT_MEMSTATUS=0
SQLITE_DEFAULT_SYNCHRONOUS=1
SQLITE_DEFAULT_WAL_SYNCHRONOUS=1
SQLITE_LIKE_DOESNT_MATCH_BLOBS
SQLITE_MAX_EXPR_DEPTH=0

View File

@ -56,7 +56,6 @@ static const char *ProjectFileSchema =
// settings.
"PRAGMA <schema>.application_id = %d;"
"PRAGMA <schema>.user_version = %d;"
"PRAGMA <schema>.journal_mode = WAL;"
""
// project is a binary representation of an XML file.
// it's in binary for speed.
@ -128,12 +127,34 @@ static const char *ProjectFileSchema =
" samples BLOB"
");";
// Settings applied to each database connection
static const char *ConnectionConfiguration =
"PRAGMA <schema>.synchronous = OFF;"
// Configuration to provide "safe" connections
static const char *SafeConfig =
"PRAGMA <schema>.locking_mode = EXCLUSIVE;"
"PRAGMA <schema>.synchronous = NORMAL;"
"PRAGMA <schema>.journal_mode = WAL;"
"PRAGMA <schema>.wal_autocheckpoint = 10000;";
// Configuration to provide "Fast" connections
static const char *FastConfig =
"PRAGMA <schema>.locking_mode = EXCLUSIVE;"
"PRAGMA <schema>.synchronous = OFF;"
"PRAGMA <schema>.journal_mode = OFF;";
// Configuration to provide "Fast" connections
static const char *FastWalConfig =
"PRAGMA <schema>.locking_mode = EXCLUSIVE;"
"PRAGMA <schema>.synchronous = OFF;"
"PRAGMA <schema>.journal_mode = WAL;"
"PRAGMA <schema>.wal_autocheckpoint = 10000;";
// Configuration to provide "Fast" connections
static const char *FastMemoryConfig =
"PRAGMA <schema>.locking_mode = EXCLUSIVE;"
"PRAGMA <schema>.synchronous = NORMAL;"
"PRAGMA <schema>.journal_mode = MEMORY;"
"PRAGMA <schema>.wal_autocheckpoint = 1000;";
// This singleton handles initialization/shutdown of the SQLite library.
// It is needed because our local SQLite is built with SQLITE_OMIT_AUTOINIT
// defined.
@ -145,7 +166,13 @@ class SQLiteIniter
public:
SQLiteIniter()
{
mRc = sqlite3_initialize();
// Enable URI filenames for all connections
mRc = sqlite3_config(SQLITE_CONFIG_URI, 1);
if (mRc == SQLITE_OK)
{
mRc = sqlite3_initialize();
}
#if !defined(__WXMSW__)
if (mRc == SQLITE_OK)
@ -161,7 +188,6 @@ public:
}
}
#endif
}
~SQLiteIniter()
{
@ -246,7 +272,6 @@ ProjectFileIO::ProjectFileIO(AudacityProject &)
mRecovered = false;
mModified = false;
mTemporary = true;
mBypass = false;
UpdatePrefs();
@ -289,10 +314,13 @@ sqlite3 *ProjectFileIO::DB()
{
if (!mDB)
{
OpenDB();
if (!mDB)
throw SimpleMessageBoxException{
XO("Failed to open the project's database") };
if (!OpenDB())
{
throw SimpleMessageBoxException
{
XO("Failed to open the project's database")
};
}
}
return mDB;
@ -358,11 +386,11 @@ void ProjectFileIO::UseConnection( sqlite3 *db, const FilePath &filePath )
SetFileName( filePath );
}
void ProjectFileIO::ConfigConnection(sqlite3 *db, const wxString &schema)
void ProjectFileIO::Config(sqlite3 *db, const char *config, const wxString &schema)
{
int rc;
wxString sql = ConnectionConfiguration;
wxString sql = config;
if (schema.empty())
{
@ -413,7 +441,7 @@ sqlite3 *ProjectFileIO::OpenDB(FilePath fileName)
}
// Ensure attached DB connection gets configured
ConfigConnection(mDB);
Config(mDB, SafeConfig);
if (!CheckVersion())
{
@ -422,6 +450,7 @@ sqlite3 *ProjectFileIO::OpenDB(FilePath fileName)
}
mTemporary = temp;
SetFileName(fileName);
return mDB;
@ -537,7 +566,7 @@ int ProjectFileIO::ExecCallback(void *data, int cols, char **vals, char **names)
return parms->func(parms->result, cols, vals, names);
}
int ProjectFileIO::Exec(const char *query, ExecCB callback, std::vector<wxString> *result)
int ProjectFileIO::Exec(const char *query, ExecCB callback, ExecResult &result)
{
char *errmsg = nullptr;
ExecParm ep = {callback, result};
@ -556,22 +585,25 @@ int ProjectFileIO::Exec(const char *query, ExecCB callback, std::vector<wxString
return rc;
}
bool ProjectFileIO::GetValues(const char *sql, std::vector<wxString> &result)
bool ProjectFileIO::Query(const char *sql, ExecResult &result)
{
result.clear();
auto getresult = [](std::vector<wxString> *result, int cols, char **vals, char **names)
auto getresult = [](ExecResult &result, int cols, char **vals, char **names)
{
if (cols == 1 && vals[0])
std::vector<wxString> row;
for (int i = 0; i < cols; ++i)
{
result->push_back(vals[0]);
return SQLITE_OK;
row.push_back(vals[i]);
}
return SQLITE_ABORT;
result.push_back(row);
return SQLITE_OK;
};
int rc = Exec(sql, getresult, &result);
int rc = Exec(sql, getresult, result);
if (rc != SQLITE_OK)
{
return false;
@ -584,15 +616,16 @@ bool ProjectFileIO::GetValue(const char *sql, wxString &result)
{
result.clear();
std::vector<wxString> holder;
if (!GetValues(sql, holder))
ExecResult holder;
if (!Query(sql, holder))
{
return false;
}
if (holder.size())
// Return the first column in the first row, if any
if (holder.size() && holder[0].size())
{
result.assign(holder[0]);
result.assign(holder[0][0]);
}
return true;
@ -614,7 +647,7 @@ bool ProjectFileIO::GetBlob(const char *sql, wxMemoryBuffer &buffer)
}
});
rc = sqlite3_prepare_v2(db, sql, -1, &stmt, 0);
rc = sqlite3_prepare_v2(db, sql, -1, &stmt, nullptr);
if (rc != SQLITE_OK)
{
SetDBError(
@ -787,7 +820,8 @@ bool ProjectFileIO::CheckForOrphans(BlockIDs &blockids)
sqlite3 *ProjectFileIO::CopyTo(const FilePath &destpath,
const TranslatableString &msg,
bool prune /* = false */)
bool prune /* = false */,
const std::shared_ptr<TrackList> &tracks/* = nullptr */)
{
// Get access to the active tracklist
auto pProject = mpProject.lock();
@ -795,7 +829,7 @@ sqlite3 *ProjectFileIO::CopyTo(const FilePath &destpath,
{
return nullptr;
}
auto &tracklist = TrackList::Get(*pProject);
auto &tracklist = tracks ? *tracks : TrackList::Get(*pProject);
BlockIDs blockids;
@ -819,8 +853,8 @@ sqlite3 *ProjectFileIO::CopyTo(const FilePath &destpath,
// Collect ALL blockids
else
{
std::vector<wxString> holder;
if (!GetValues("SELECT blockid FROM sampleblocks;", holder))
ExecResult holder;
if (!Query("SELECT blockid FROM sampleblocks;", holder))
{
return nullptr;
}
@ -828,7 +862,7 @@ sqlite3 *ProjectFileIO::CopyTo(const FilePath &destpath,
for (auto block : holder)
{
SampleBlockID blockid;
block.ToLongLong(&blockid);
block[0].ToLongLong(&blockid);
blockids.insert(blockid);
}
@ -867,7 +901,7 @@ sqlite3 *ProjectFileIO::CopyTo(const FilePath &destpath,
}
// Ensure attached DB connection gets configured
ConfigConnection(db, "outbound");
Config(db, FastConfig, "outbound");
// Install our schema into the new database
if (!InstallSchema(db, "outbound"))
@ -876,6 +910,7 @@ sqlite3 *ProjectFileIO::CopyTo(const FilePath &destpath,
return nullptr;
}
// Copy over tags (not really used yet)
rc = sqlite3_exec(db,
"INSERT INTO outbound.tags SELECT * FROM main.tags;",
nullptr,
@ -906,7 +941,9 @@ sqlite3 *ProjectFileIO::CopyTo(const FilePath &destpath,
"INSERT INTO outbound.sampleblocks"
" SELECT * FROM main.sampleblocks"
" WHERE blockid = ?;",
-1, &stmt, 0);
-1,
&stmt,
nullptr);
if (rc != SQLITE_OK)
{
SetDBError(
@ -923,6 +960,12 @@ sqlite3 *ProjectFileIO::CopyTo(const FilePath &destpath,
wxLongLong_t count = 0;
wxLongLong_t total = blockids.size();
// Start a transaction. Since we're running without a journal,
// this really doesn't provide rollback. It just prevents SQLite
// from auto committing after each step through the loop.
sqlite3_exec(db, "BEGIN;", nullptr, nullptr, nullptr);
// Copy sample blocks from the main DB to the outbound DB
for (auto blockid : blockids)
{
// BIND blockid parameter
@ -955,21 +998,22 @@ sqlite3 *ProjectFileIO::CopyTo(const FilePath &destpath,
return nullptr;
}
}
// See BEGIN above...
sqlite3_exec(db, "COMMIT;", nullptr, nullptr, nullptr);
}
// Copy failed
if (rc != SQLITE_DONE)
// Detach the destination database
rc = sqlite3_exec(db, "DETACH DATABASE outbound;", nullptr, nullptr, nullptr);
if (rc != SQLITE_OK)
{
SetDBError(
XO("Failed to copy project file")
XO("Destination project could not be detached")
);
return nullptr;
}
// Detach the destination database
sqlite3_exec(db, "DETACH DATABASE outbound;", nullptr, nullptr, nullptr);
// Open the newly created database
rc = sqlite3_open(destpath, &destdb);
if (rc != SQLITE_OK)
@ -981,32 +1025,28 @@ sqlite3 *ProjectFileIO::CopyTo(const FilePath &destpath,
return nullptr;
}
// Ensure connection gets configured
ConfigConnection(destdb);
// Ensure attached DB connection gets configured
Config(destdb, FastWalConfig);
// Tell cleanup everything is good to go
success = true;
// IMPORTANT: At this point, the DB handle is setup with WAL journaling,
// but synchronous is OFF. Only use this connection if you can tolerate
// a crash/power failure. Otherwise, reconfigure it using SafeConfig.
//
// It's left configured this way so that the caller can do additional
// work using a faster, but VOLATILE connection.
return destdb;
}
unsigned long long ProjectFileIO::CalculateUsage()
bool ProjectFileIO::ShouldVacuum(const std::shared_ptr<TrackList> &tracks)
{
// Collect all active block usage
auto pProject = mpProject.lock();
if (!pProject)
{
return false;
}
auto &tracklist = TrackList::Get(*pProject);
std::set<long long> active;
unsigned long long current = 0;
std::set<long long> seen;
unsigned long long result = 0;
//TIMER_START( "CalculateSpaceUsage", space_calc );
for (auto wt : tracklist.Any<const WaveTrack>())
// Scan all wave tracks
for (auto wt : tracks->Any<const WaveTrack>())
{
// Scan all clips within current track
for (const auto &clip : wt->GetAllClips())
@ -1016,56 +1056,86 @@ unsigned long long ProjectFileIO::CalculateUsage()
for (const auto &block : *blocks)
{
const auto &sb = block.sb;
auto blockid = sb->GetBlockID();
// Accumulate space used by the blockid if the blocckid was not
// yet seen
if (seen.count( sb->GetBlockID()) == 0)
// Accumulate space used by the block if the blocckid has not
// yet been seen
if (active.count(blockid) == 0)
{
result += sb->GetSpaceUsage();
}
current += sb->GetSpaceUsage();
// Add file to current set
seen.insert(sb->GetBlockID());
active.insert(blockid);
}
}
}
}
return result;
}
bool ProjectFileIO::Vacuum()
{
double used = (double) CalculateUsage();
// Collect total usage
wxString result;
if (!GetValue("SELECT sum(length(samples)) FROM sampleblocks;", result))
// Get the number of blocks and total length from the project file.
ExecResult holder;
if (!Query("SELECT Count(*), Sum(Length(summary256)) + Sum(Length(summary64k)) + Sum(Length(samples)) FROM sampleblocks;", holder))
{
// Shouldn't vacuum since we don't have the full picture
return false;
}
double total;
result.ToDouble(&total);
wxLogDebug(wxT("used = %f total = %f %f\n"), used, total, used / total);
if (used / total > 0.80)
// Verify we got the results we asked for
if (holder.size() != 1 || holder[0].size() != 2)
{
// Shouldn't vacuum since we don't have the full picture
return false;
}
// Convert
unsigned long long blockcount = 0;
holder[0][0].ToULongLong(&blockcount);
unsigned long long total = 0;
holder[0][1].ToULongLong(&total);
// Remember if we had unused blocks in the project file
mHadUnused = (blockcount > active.size());
// Let's make a percentage...should be plenty of head room
current *= 100;
wxLogDebug(wxT("used = %lld total = %lld %lld\n"), current, total, current / total);
if (current / total > 80)
{
wxLogDebug(wxT("not vacuuming"));
return true;
return false;
}
wxLogDebug(wxT("vacuuming"));
wxString origName = mFileName;
wxString tempName = origName + "_vacuum";
return true;
}
void ProjectFileIO::Vacuum(const std::shared_ptr<TrackList> &tracks)
{
// Haven't vacuumed yet
mWasVacuumed = false;
// Assume we do until we found out othersize. That way cleanup at project
// close time will still occur
mHadUnused = true;
// Don't vacuum if this is a temporary project or if Go figure out if we should at least try
if (IsTemporary() || !ShouldVacuum(tracks))
{
return;
}
// Create the project doc
ProjectSerializer doc;
WriteXMLHeader(doc);
WriteXML(doc);
WriteXML(doc, false, tracks);
wxString origName = mFileName;
wxString tempName = origName + "_vacuum";
// Must close the database to rename it
if (!CloseDB())
{
return false;
return;
}
// Shouldn't need to do this, but doesn't hurt.
@ -1076,35 +1146,43 @@ bool ProjectFileIO::Vacuum()
{
OpenDB(origName);
return false;
return;
}
// If we can't reopen the original database using the temporary name, backout
if (sqlite3_open(tempName, &mDB) != SQLITE_OK)
sqlite3 *tempDB = nullptr;
if (sqlite3_open(tempName, &tempDB) != SQLITE_OK)
{
SetDBError(XO("Failed to open project file"));
// sqlite3 docs say you should close anyway to avoid leaks
sqlite3_close( mDB );
sqlite3_close( tempDB );
wxRenameFile(tempName, origName);
OpenDB(origName);
return false;
return;
}
UseConnection(tempDB, tempName);
// Ensure connection gets configured
ConfigConnection(mDB);
Config(mDB, SafeConfig);
// Copy the original database to a new database while pruning unused sample blocks
auto newDB = CopyTo(origName, XO("Compacting project"), true);
auto newDB = CopyTo(origName, XO("Compacting project"), true, tracks);
// Close handle to the original database
CloseDB();
// Reestablish the original name. No need to reopen as it will happen later,
// if needed.
UseConnection(newDB, origName);
// If the copy failed or we aren't able to write the project doc, backout
if (!newDB || !WriteDoc("project", doc, newDB))
{
// Close the new and original DBs
// Close the new database
sqlite3_close(newDB);
CloseDB();
// AUD3 warn user somehow
wxRemoveFile(origName);
@ -1112,19 +1190,27 @@ bool ProjectFileIO::Vacuum()
// AUD3 warn user somehow
wxRenameFile(tempName, origName);
// Reopen the original DB
OpenDB(origName);
UseConnection(nullptr, origName);
return false;
return;
}
CloseDB();
wxRemoveFile(tempName);
UseConnection(newDB, origName);
// Remember that we vacuumed
mWasVacuumed = true;
return true;
return;
}
bool ProjectFileIO::WasVacuumed()
{
return mWasVacuumed;
}
bool ProjectFileIO::HadUnused()
{
return mHadUnused;
}
void ProjectFileIO::UpdatePrefs()
@ -1368,14 +1454,16 @@ void ProjectFileIO::WriteXMLHeader(XMLWriter &xmlFile) const
xmlFile.Write(wxT(">\n"));
}
void ProjectFileIO::WriteXML(XMLWriter &xmlFile, bool recording)
void ProjectFileIO::WriteXML(XMLWriter &xmlFile,
bool recording /* = false */,
const std::shared_ptr<TrackList> &tracks /* = nullptr */)
// may throw
{
auto pProject = mpProject.lock();
if (! pProject )
THROW_INCONSISTENCY_EXCEPTION;
auto &proj = *pProject;
auto &tracklist = TrackList::Get(proj);
auto &tracklist = tracks ? *tracks : TrackList::Get(proj);
auto &viewInfo = ViewInfo::Get(proj);
auto &tags = Tags::Get(proj);
const auto &settings = ProjectSettings::Get(proj);
@ -1459,6 +1547,8 @@ bool ProjectFileIO::AutoSaveDelete(sqlite3 *db /* = nullptr */)
return false;
}
mModified = false;
return true;
}
@ -1491,7 +1581,7 @@ bool ProjectFileIO::WriteDoc(const char *table,
}
});
rc = sqlite3_prepare_v2(db, sql, -1, &stmt, 0);
rc = sqlite3_prepare_v2(db, sql, -1, &stmt, nullptr);
if (rc != SQLITE_OK)
{
SetDBError(
@ -1532,89 +1622,72 @@ bool ProjectFileIO::WriteDoc(const char *table,
// IDs.
bool ProjectFileIO::ImportProject(const FilePath &fileName)
{
// Get access to the active tracklist
auto pProject = mpProject.lock();
if (!pProject)
{
return false;
}
// Get access to the current project file
auto db = DB();
bool success = false;
bool restore = true;
int rc;
// We need either the autosave or project docs from the inbound AUP3
wxString project;
// Ensure the inbound database gets detached
auto detach = finally([&]
{
sqlite3 *indb = nullptr;
sqlite3_exec(db, "DETACH DATABASE inbound;", nullptr, nullptr, nullptr);
});
// Make sure we always return to the active project file
auto cleanup = finally([&]
{
if (indb)
{
DiscardConnection();
}
// Attach the inbound project file
wxString sql;
sql.Printf("ATTACH DATABASE 'file:%s?immutable=1&mode=ro' AS inbound;", fileName);
RestoreConnection();
});
SaveConnection();
rc = sqlite3_exec(db, sql.mb_str().data(), nullptr, nullptr, nullptr);
if (rc != SQLITE_OK)
{
SetDBError(
XO("Unable to attach %s project file").Format(fileName)
);
// Would be nice if we could open it read-only, but SQLITE_IOERR is
// returned when doing so.
rc = sqlite3_open(fileName, &indb);
if (rc != SQLITE_OK)
{
SetDBError(
XO("Unable to open project file:\n\n%s").Format(fileName)
);
return false;
}
return false;
}
// We need either the autosave or project docs from the inbound AUP3
wxMemoryBuffer buffer;
// Ensure connection gets configured
ConfigConnection(indb);
// Get the autosave doc, if any
if (!GetBlob("SELECT dict || doc FROM inbound.project WHERE id = 1;", buffer))
{
// Error already set
return false;
}
// The inbound project file becomes the active project file
UseConnection(indb, fileName);
BlockIDs blockids;
wxMemoryBuffer buffer;
// Get the autosave doc, if any
if (!GetBlob("SELECT dict || doc FROM project WHERE id = 1;", buffer))
// If we didn't have an autosave doc, load the project doc instead
if (buffer.GetDataLen() == 0)
{
if (!GetBlob("SELECT dict || doc FROM inbound.autosave WHERE id = 1;", buffer))
{
// Error already set
return false;
}
// If we didn't have an autosave doc, load the project doc instead
if (buffer.GetDataLen() == 0)
// Missing both the autosave and project docs...this shouldn't happen!!!
if (buffer.GetDataLen() > 0)
{
if (!GetBlob("SELECT dict || doc FROM autosave WHERE id = 1;", buffer))
{
// Error already set
return false;
}
// Missing both the autosave and project docs...this shouldn't happen!!!
if (buffer.GetDataLen() > 0)
{
SetError(XO("Unable to load project or autosave documents"));
return false;
}
}
// Decode it while capturing the associated sample blockids
project = ProjectSerializer::Decode(buffer, blockids);
if (project.size() == 0)
{
SetError(XO("Unable to decode project document"));
SetError(XO("Unable to load project or autosave documents"));
return false;
}
}
wxString project;
BlockIDs blockids;
// Decode it while capturing the associated sample blockids
project = ProjectSerializer::Decode(buffer, blockids);
if (project.size() == 0)
{
SetError(XO("Unable to decode project document"));
return false;
}
// Parse the project doc
wxStringInputStream in(project);
wxXmlDocument doc;
@ -1661,9 +1734,15 @@ bool ProjectFileIO::ImportProject(const FilePath &fileName)
}
};
// Get access to the active tracklist
auto pProject = mpProject.lock();
if (!pProject)
{
return false;
}
auto &tracklist = TrackList::Get(*pProject);
// Search for a timetrack and remove if the project already has one
// Search for a timetrack and remove it if the project already has one
if (*tracklist.Any<TimeTrack>().begin())
{
// Find a timetrack and remove it if it exists
@ -1693,9 +1772,6 @@ bool ProjectFileIO::ImportProject(const FilePath &fileName)
}
{
// Get access to the current project file
auto db = DB();
// Cleanup...
sqlite3_stmt *stmt = nullptr;
auto cleanup = finally([&]
@ -1705,28 +1781,8 @@ bool ProjectFileIO::ImportProject(const FilePath &fileName)
{
sqlite3_finalize(stmt);
}
// Detach the inbound project file, whether it was successfully attached or not
sqlite3_exec(db, "DETACH DATABASE inbound;", nullptr, nullptr, nullptr);
});
// Attach the inbound project file
wxString sql;
sql.Printf("ATTACH DATABASE '%s' AS inbound;", fileName);
rc = sqlite3_exec(db, sql.mb_str().data(), nullptr, nullptr, nullptr);
if (rc != SQLITE_OK)
{
SetDBError(
XO("Unable to attach %s project file").Format(fileName)
);
return false;
}
// Ensure attached DB connection gets configured
ConfigConnection(db, "inbound");
// Prepare the statement to copy the sample block from the inbound project to the
// active project. All columns other than the blockid column gets copied.
wxString columns(wxT("sampleformat, summin, summax, sumrms, summary256, summary64k, samples"));
@ -1737,7 +1793,7 @@ bool ProjectFileIO::ImportProject(const FilePath &fileName)
columns,
columns);
rc = sqlite3_prepare_v2(db, sql, -1, &stmt, 0);
rc = sqlite3_prepare_v2(db, sql, -1, &stmt, nullptr);
if (rc != SQLITE_OK)
{
SetDBError(
@ -1814,7 +1870,7 @@ bool ProjectFileIO::ImportProject(const FilePath &fileName)
// Replace the original blockid with the new one
attr->SetValue(wxString::Format(wxT("%lld"), sqlite3_last_insert_rowid(db)));
// BIND blockid parameter
// Reset the statement for the next iteration
if (sqlite3_reset(stmt) != SQLITE_OK)
{
THROW_INCONSISTENCY_EXCEPTION;
@ -1849,8 +1905,6 @@ bool ProjectFileIO::ImportProject(const FilePath &fileName)
return false;
}
// We're all done with the inbound project file...finally() will detach it
}
// Recreate the project doc with the revisions we've made above
@ -2042,7 +2096,12 @@ bool ProjectFileIO::SaveProject(const FilePath &fileName)
// (also ensuring closing of one of the connections, with the cooperation
// of the finally above)
SaveConnection();
UseConnection( newDB, fileName );
// Make the new connection "safe"
Config(newDB, SafeConfig);
// And make it the active project file
UseConnection(newDB, fileName);
}
ProjectSerializer doc;
@ -2054,12 +2113,12 @@ bool ProjectFileIO::SaveProject(const FilePath &fileName)
return false;
}
// Autosave no longer needed
AutoSaveDelete();
// Reaching this point defines success and all the rest are no-fail
// operations:
// Tell the finally block to behave
success = true;
// No longer modified
mModified = false;
@ -2072,6 +2131,9 @@ bool ProjectFileIO::SaveProject(const FilePath &fileName)
// Adjust the title
SetProjectTitle();
// Tell the finally block to behave
success = true;
return true;
}
@ -2173,11 +2235,15 @@ void ProjectFileIO::SetDBError(const TranslatableString &msg)
{
mLastError = msg;
wxLogDebug(wxT("SQLite error: %s"), mLastError.Debug());
printf(" Lib error: %s", mLastError.Debug().mb_str().data());
if (mDB)
{
mLibraryError = Verbatim(sqlite3_errmsg(mDB));
wxLogDebug(wxT(" Lib error: %s"), mLibraryError.Debug());
printf(" Lib error: %s", mLibraryError.Debug().mb_str().data());
}
wxASSERT(false);
}
void ProjectFileIO::Bypass(bool bypass)
@ -2187,7 +2253,7 @@ void ProjectFileIO::Bypass(bool bypass)
bool ProjectFileIO::ShouldBypass()
{
return mTemporary && mBypass;
return mBypass;
}
AutoCommitTransaction::AutoCommitTransaction(ProjectFileIO &projectFileIO,

View File

@ -80,10 +80,6 @@ public:
bool SaveProject(const FilePath &fileName);
bool SaveCopy(const FilePath& fileName);
XMLTagHandler *HandleXMLChild(const wxChar *tag) override;
void WriteXMLHeader(XMLWriter &xmlFile) const;
void WriteXML(XMLWriter &xmlFile, bool recording = false) /* not override */;
wxLongLong GetFreeDiskSpace();
const TranslatableString &GetLastError() const;
@ -105,22 +101,33 @@ public:
bool ShouldBypass();
// Remove all unused space within a project file
bool Vacuum();
void Vacuum(const std::shared_ptr<TrackList> &tracks);
// The last vacuum check did actually vacuum the project file if true
bool WasVacuumed();
// The last vacuum check found unused blocks in the project file
bool HadUnused();
private:
void WriteXMLHeader(XMLWriter &xmlFile) const;
void WriteXML(XMLWriter &xmlFile, bool recording = false, const std::shared_ptr<TrackList> &tracks = nullptr) /* not override */;
// XMLTagHandler callback methods
bool HandleXMLTag(const wxChar *tag, const wxChar **attrs) override;
XMLTagHandler *HandleXMLChild(const wxChar *tag) override;
void UpdatePrefs() override;
using ExecCB = std::function<int(std::vector<wxString> *result, int cols, char **vals, char **names)>;
using ExecResult = std::vector<std::vector<wxString>>;
using ExecCB = std::function<int(ExecResult &result, int cols, char **vals, char **names)>;
struct ExecParm
{
ExecCB func;
std::vector<wxString> *result;
ExecResult &result;
};
static int ExecCallback(void *data, int cols, char **vals, char **names);
int Exec(const char *query, ExecCB callback, std::vector<wxString> *result);
int Exec(const char *query, ExecCB callback, ExecResult &result);
// The opening of the database may be delayed until demanded.
// Returns a non-null pointer to an open database, or throws an exception
@ -141,7 +148,7 @@ private:
void UseConnection(sqlite3 *db, const FilePath &filePath);
// Make sure the connection/schema combo is configured the way we want
void ConfigConnection(sqlite3 *db, const wxString &schema = wxT("main"));
void Config(sqlite3 *db, const char *config, const wxString &schema = wxT("main"));
sqlite3 *OpenDB(FilePath fileName = {});
bool CloseDB();
@ -151,7 +158,8 @@ private:
bool TransactionCommit(const wxString &name);
bool TransactionRollback(const wxString &name);
bool GetValues(const char *sql, std::vector<wxString> &value);
bool Query(const char *sql, ExecResult &result);
bool GetValue(const char *sql, wxString &value);
bool GetBlob(const char *sql, wxMemoryBuffer &buffer);
@ -172,12 +180,13 @@ private:
// Return a database connection if successful, which caller must close
sqlite3 *CopyTo(const FilePath &destpath,
const TranslatableString &msg,
bool prune = false);
bool prune = false,
const std::shared_ptr<TrackList> &tracks = nullptr);
void SetError(const TranslatableString & msg);
void SetDBError(const TranslatableString & msg);
unsigned long long CalculateUsage();
bool ShouldVacuum(const std::shared_ptr<TrackList> &tracks);
private:
// non-static data members
@ -198,6 +207,12 @@ private:
// Bypass transactions if database will be deleted after close
bool mBypass;
// Project was vacuumed last time Vacuum() ran
bool mWasVacuumed;
// Project had unused blocks during last Vacuum()
bool mHadUnused;
sqlite3 *mPrevDB;
FilePath mPrevFileName;

View File

@ -670,11 +670,17 @@ void ProjectFileManager::CloseLock()
// there's no memory leak.
if (mLastSavedTracks)
{
auto &project = mProject;
auto &projectFileIO = ProjectFileIO::Get(project);
for (auto wt : mLastSavedTracks->Any<WaveTrack>())
{
wt->CloseLock();
}
// Attempt to vacuum the project
projectFileIO.Vacuum(mLastSavedTracks);
mLastSavedTracks->Clear();
mLastSavedTracks.reset();
}

View File

@ -670,29 +670,6 @@ void ProjectManager::OnCloseWindow(wxCloseEvent & event)
}
}
}
// Cleanup the project file
//
// Might be that we want to UndoManager::ClearStates() before this???
bool vacuumed = false;
if (!projectFileIO.IsTemporary() && settings.GetCompactAtClose())
{
vacuumed = projectFileIO.Vacuum();
}
// See ProjectFileIO::Bypass() for a description
projectFileIO.Bypass(true);
// Not sure if this has to come later in the shutdown process, but doing
// it before the project window is destroyed looks better when huge states
// need to be cleaned up.
if (!vacuumed)
{
// This must be done before the following Deref() since it holds
// references to the DirManager.
UndoManager::Get( project ).ClearStates();
}
#ifdef __WXMAC__
// Fix bug apparently introduced into 2.1.2 because of wxWidgets 3:
// closing a project that was made full-screen (as by clicking the green dot
@ -710,13 +687,6 @@ void ProjectManager::OnCloseWindow(wxCloseEvent & event)
// Stop the timer since there's no need to update anything anymore
mTimer.reset();
// The project is now either saved or the user doesn't want to save it,
// so there's no need to keep auto save info around anymore
// PRL: not clear what to do if the following fails, but the worst should
// be, the project may reopen in its present state as a recovery file, not
// at the last saved state.
(void) projectFileIO.AutoSaveDelete();
// DMM: Save the size of the last window the user closes
//
// LL: Save before doing anything else to the window that might make
@ -764,8 +734,56 @@ void ProjectManager::OnCloseWindow(wxCloseEvent & event)
// TODO: Is there a Mac issue here??
// SetMenuBar(NULL);
// Lock active sample blocks so they don't get deleted below. This also performs
// a vacuum if necessary.
projectFileManager.CloseLock();
// Determine if we can bypass sample block deletes during shutdown.
//
// IMPORTANT:
// If the project was vacuumed, then we MUST bypass further
// deletions since the new file doesn't have the blocks that the
// Sequences expect to be there.
bool bypass = true;
// Only permanent project files need cleaning at shutdown
if (!projectFileIO.IsTemporary() && !projectFileIO.WasVacuumed())
{
// Delete the AutoSave doc it if still exists
if (projectFileIO.IsModified())
{
// The user doesn't want to save the project, so delete the AutoSave doc
// PRL: not clear what to do if the following fails, but the worst should
// be, the project may reopen in its present state as a recovery file, not
// at the last saved state.
(void) projectFileIO.AutoSaveDelete();
}
// If we still have unused blocks, then we must not bypass deletions
// during shutdown. Otherwise, we would have orphaned blocks the next time
// the project is opened.
//
// An example of when dead blocks will exist is when a user opens a permanent
// project, adds a track (with samples) to it, and chooses not to save the
// changes.
if (projectFileIO.HadUnused())
{
bypass = false;
}
}
projectFileIO.Bypass(bypass);
{
AutoCommitTransaction trans(projectFileIO, "Shutdown");
// This must be done before the following Deref() since it holds
// references to the DirManager.
UndoManager::Get( project ).ClearStates();
// Delete all the tracks to free up memory and DirManager references.
tracks.Clear();
}
// Some of the AdornedRulerPanel functions refer to the TrackPanel, so destroy this
// before the TrackPanel is destroyed. This change was needed to stop Audacity
// crashing when running with Jaws on Windows 10 1703.
@ -785,9 +803,6 @@ void ProjectManager::OnCloseWindow(wxCloseEvent & event)
TrackFactory::Destroy( project );
// Delete all the tracks to free up memory and DirManager references.
tracks.Clear();
// Remove self from the global array, but defer destruction of self
auto pSelf = AllProjects{}.Remove( project );
wxASSERT( pSelf );