Redesign the undo stack

Store the data in vectors rather than AssFiles since even an intrusive
linked list has comically high memory overhead. Cuts memory usage of a
full undo stack with 15k lines by 65 MB for 32-bit and 130 MB for
64-bit. Also roughly halves how long it takes to copy the file for the
undo stack, and makes undo/redo a bit faster.
This commit is contained in:
Thomas Goyne 2014-03-04 08:32:29 -08:00
parent 2a316e5a55
commit 9ecb54333a
8 changed files with 131 additions and 81 deletions

View file

@ -53,34 +53,24 @@ using namespace boost::adaptors;
static int next_id = 0; static int next_id = 0;
AssDialogue::AssDialogue() AssDialogue::AssDialogue() {
: Id(++next_id) Id = ++next_id;
{
memset(Margin, 0, sizeof Margin);
} }
AssDialogue::AssDialogue(AssDialogue const& that) AssDialogue::AssDialogue(AssDialogue const& that) : AssDialogueBase(that) {
: Id(++next_id) Id = ++next_id;
, Comment(that.Comment)
, Layer(that.Layer)
, Start(that.Start)
, End(that.End)
, Style(that.Style)
, Actor(that.Actor)
, Effect(that.Effect)
, Text(that.Text)
{
memmove(Margin, that.Margin, sizeof Margin);
} }
AssDialogue::AssDialogue(std::string const& data) AssDialogue::AssDialogue(AssDialogueBase const& that) : AssDialogueBase(that) {
: Id(++next_id) Id = ++next_id;
{ }
AssDialogue::AssDialogue(std::string const& data) {
Id = ++next_id;
Parse(data); Parse(data);
} }
AssDialogue::~AssDialogue () { AssDialogue::~AssDialogue () { }
}
class tokenizer { class tokenizer {
agi::StringRange str; agi::StringRange str;
@ -177,14 +167,6 @@ std::string AssDialogue::GetData(bool ssa) const {
return str; return str;
} }
const std::string AssDialogue::GetEntryData() const {
return GetData(false);
}
std::string AssDialogue::GetSSAText() const {
return GetData(true);
}
std::auto_ptr<boost::ptr_vector<AssDialogueBlock>> AssDialogue::ParseTags() const { std::auto_ptr<boost::ptr_vector<AssDialogueBlock>> AssDialogue::ParseTags() const {
boost::ptr_vector<AssDialogueBlock> Blocks; boost::ptr_vector<AssDialogueBlock> Blocks;
@ -278,6 +260,6 @@ std::string AssDialogue::GetStrippedText() const {
AssEntry *AssDialogue::Clone() const { AssEntry *AssDialogue::Clone() const {
auto clone = new AssDialogue(*this); auto clone = new AssDialogue(*this);
*const_cast<int *>(&clone->Id) = Id; clone->Id = Id;
return clone; return clone;
} }

View file

@ -38,6 +38,7 @@
#include <libaegisub/exception.h> #include <libaegisub/exception.h>
#include <array>
#include <boost/flyweight.hpp> #include <boost/flyweight.hpp>
#include <boost/ptr_container/ptr_vector.hpp> #include <boost/ptr_container/ptr_vector.hpp>
#include <vector> #include <vector>
@ -123,24 +124,18 @@ public:
void ProcessParameters(ProcessParametersCallback callback, void *userData); void ProcessParameters(ProcessParametersCallback callback, void *userData);
}; };
class AssDialogue : public AssEntry { struct AssDialogueBase {
std::string GetData(bool ssa) const;
/// @brief Parse raw ASS data into everything else
/// @param data ASS line
void Parse(std::string const& data);
public:
/// Unique ID of this line. Copies of the line for Undo/Redo purposes /// Unique ID of this line. Copies of the line for Undo/Redo purposes
/// preserve the unique ID, so that the equivalent lines can be found in /// preserve the unique ID, so that the equivalent lines can be found in
/// the different versions of the file. /// the different versions of the file.
const int Id; int Id;
/// Is this a comment line? /// Is this a comment line?
bool Comment = false; bool Comment = false;
/// Layer number /// Layer number
int Layer = 0; int Layer = 0;
/// Margins: 0 = Left, 1 = Right, 2 = Top (Vertical) /// Margins: 0 = Left, 1 = Right, 2 = Top (Vertical)
int Margin[3]; std::array<int, 3> Margin = {{0, 0, 0}};
/// Starting time /// Starting time
AssTime Start = 0; AssTime Start = 0;
/// Ending time /// Ending time
@ -153,7 +148,15 @@ public:
boost::flyweight<std::string> Effect; boost::flyweight<std::string> Effect;
/// Raw text data /// Raw text data
boost::flyweight<std::string> Text; boost::flyweight<std::string> Text;
};
class AssDialogue : public AssEntry, public AssDialogueBase {
std::string GetData(bool ssa) const;
/// @brief Parse raw ASS data into everything else
/// @param data ASS line
void Parse(std::string const& data);
public:
AssEntryGroup Group() const override { return AssEntryGroup::DIALOGUE; } AssEntryGroup Group() const override { return AssEntryGroup::DIALOGUE; }
/// Parse text as ASS and return block information /// Parse text as ASS and return block information
@ -167,10 +170,10 @@ public:
/// Update the text of the line from parsed blocks /// Update the text of the line from parsed blocks
void UpdateText(boost::ptr_vector<AssDialogueBlock>& blocks); void UpdateText(boost::ptr_vector<AssDialogueBlock>& blocks);
const std::string GetEntryData() const override; const std::string GetEntryData() const override { return GetData(false); }
/// Get the line as SSA rather than ASS /// Get the line as SSA rather than ASS
std::string GetSSAText() const override; std::string GetSSAText() const override { return GetData(true); }
/// Does this line collide with the passed line? /// Does this line collide with the passed line?
bool CollidesWith(const AssDialogue *target) const; bool CollidesWith(const AssDialogue *target) const;
@ -178,6 +181,7 @@ public:
AssDialogue(); AssDialogue();
AssDialogue(AssDialogue const&); AssDialogue(AssDialogue const&);
AssDialogue(AssDialogueBase const&);
AssDialogue(std::string const& data); AssDialogue(std::string const& data);
~AssDialogue(); ~AssDialogue();
}; };

View file

@ -206,8 +206,7 @@ AssStyle *AssFile::GetStyle(std::string const& name) {
} }
int AssFile::Commit(wxString const& desc, int type, int amend_id, AssEntry *single_line) { int AssFile::Commit(wxString const& desc, int type, int amend_id, AssEntry *single_line) {
AssFileCommit c = { desc, &amend_id, single_line }; PushState({desc, &amend_id, single_line});
PushState(c);
std::set<const AssEntry*> changed_lines; std::set<const AssEntry*> changed_lines;
if (single_line) if (single_line)

View file

@ -23,7 +23,7 @@ class AssInfo : public AssEntry {
std::string value; std::string value;
public: public:
AssInfo(AssInfo const& o) : key(o.key), value(o.value) { } AssInfo(AssInfo const& o) = default;
AssInfo(std::string key, std::string value) : key(std::move(key)), value(std::move(value)) { } AssInfo(std::string key, std::string value) : key(std::move(key)), value(std::move(value)) { }
AssEntry *Clone() const override { return new AssInfo(*this); } AssEntry *Clone() const override { return new AssInfo(*this); }

View file

@ -34,17 +34,17 @@
static const size_t bad_pos = -1; static const size_t bad_pos = -1;
namespace { namespace {
auto get_dialogue_field(SearchReplaceSettings::Field field) -> decltype(&AssDialogue::Text) { auto get_dialogue_field(SearchReplaceSettings::Field field) -> decltype(&AssDialogueBase::Text) {
switch (field) { switch (field) {
case SearchReplaceSettings::Field::TEXT: return &AssDialogue::Text; case SearchReplaceSettings::Field::TEXT: return &AssDialogueBase::Text;
case SearchReplaceSettings::Field::STYLE: return &AssDialogue::Style; case SearchReplaceSettings::Field::STYLE: return &AssDialogueBase::Style;
case SearchReplaceSettings::Field::ACTOR: return &AssDialogue::Actor; case SearchReplaceSettings::Field::ACTOR: return &AssDialogueBase::Actor;
case SearchReplaceSettings::Field::EFFECT: return &AssDialogue::Effect; case SearchReplaceSettings::Field::EFFECT: return &AssDialogueBase::Effect;
} }
throw agi::InternalError("Bad field for search", nullptr); throw agi::InternalError("Bad field for search", nullptr);
} }
std::string const& get_normalized(const AssDialogue *diag, decltype(&AssDialogue::Text) field) { std::string const& get_normalized(const AssDialogue *diag, decltype(&AssDialogueBase::Text) field) {
auto& value = const_cast<AssDialogue*>(diag)->*field; auto& value = const_cast<AssDialogue*>(diag)->*field;
auto normalized = boost::locale::normalize(value.get()); auto normalized = boost::locale::normalize(value.get());
if (normalized != value) if (normalized != value)
@ -55,7 +55,7 @@ std::string const& get_normalized(const AssDialogue *diag, decltype(&AssDialogue
typedef std::function<MatchState (const AssDialogue*, size_t)> matcher; typedef std::function<MatchState (const AssDialogue*, size_t)> matcher;
class noop_accessor { class noop_accessor {
boost::flyweight<std::string> AssDialogue::*field; boost::flyweight<std::string> AssDialogueBase::*field;
size_t start; size_t start;
public: public:
@ -72,7 +72,7 @@ public:
}; };
class skip_tags_accessor { class skip_tags_accessor {
boost::flyweight<std::string> AssDialogue::*field; boost::flyweight<std::string> AssDialogueBase::*field;
std::vector<std::pair<size_t, size_t>> blocks; std::vector<std::pair<size_t, size_t>> blocks;
size_t start; size_t start;

View file

@ -18,8 +18,10 @@
#include "subs_controller.h" #include "subs_controller.h"
#include "ass_attachment.h"
#include "ass_dialogue.h" #include "ass_dialogue.h"
#include "ass_file.h" #include "ass_file.h"
#include "ass_info.h"
#include "ass_style.h" #include "ass_style.h"
#include "charset_detect.h" #include "charset_detect.h"
#include "compat.h" #include "compat.h"
@ -50,10 +52,73 @@ namespace {
} }
struct SubsController::UndoInfo { struct SubsController::UndoInfo {
AssFile file; std::vector<std::pair<std::string, std::string>> script_info;
std::vector<AssStyle> styles;
std::vector<AssDialogueBase> events;
std::vector<AssAttachment> graphics;
std::vector<AssAttachment> fonts;
wxString undo_description; wxString undo_description;
int commit_id; int commit_id;
UndoInfo(AssFile const& f, wxString const& d, int c) : file(f), undo_description(d), commit_id(c) { } UndoInfo(AssFile const& f, wxString const& d, int c)
: undo_description(d), commit_id(c)
{
size_t info_count = 0, style_count = 0, event_count = 0, font_count = 0, graphics_count = 0;
for (auto const& line : f.Line) {
switch (line.Group()) {
case AssEntryGroup::DIALOGUE: ++event_count; break;
case AssEntryGroup::INFO: ++info_count; break;
case AssEntryGroup::STYLE: ++style_count; break;
case AssEntryGroup::FONT: ++font_count; break;
case AssEntryGroup::GRAPHIC: ++graphics_count; break;
default: assert(false); break;
}
}
script_info.reserve(info_count);
styles.reserve(style_count);
events.reserve(event_count);
for (auto const& line : f.Line) {
switch (line.Group()) {
case AssEntryGroup::DIALOGUE:
events.push_back(static_cast<AssDialogue const&>(line));
break;
case AssEntryGroup::INFO: {
auto info = static_cast<const AssInfo *>(&line);
script_info.emplace_back(info->Key(), info->Value());
break;
}
case AssEntryGroup::STYLE:
styles.push_back(static_cast<AssStyle const&>(line));
break;
case AssEntryGroup::FONT:
fonts.push_back(static_cast<AssAttachment const&>(line));
break;
case AssEntryGroup::GRAPHIC:
graphics.push_back(static_cast<AssAttachment const&>(line));
break;
default:
assert(false);
break;
}
}
}
operator AssFile() const {
AssFile ret;
for (auto const& info : script_info)
ret.Line.push_back(*new AssInfo(info.first, info.second));
for (auto const& style : styles)
ret.Line.push_back(*new AssStyle(style));
for (auto const& event : events)
ret.Line.push_back(*new AssDialogue(event));
for (auto const& attachment : graphics)
ret.Line.push_back(*new AssAttachment(attachment));
for (auto const& attachment : fonts)
ret.Line.push_back(*new AssAttachment(attachment));
return ret;
}
}; };
SubsController::SubsController(agi::Context *context) SubsController::SubsController(agi::Context *context)
@ -275,22 +340,21 @@ void SubsController::OnCommit(AssFileCommit c) {
// saved since the last change // saved since the last change
if (commit_id == *c.commit_id+1 && redo_stack.empty() && saved_commit_id+1 != commit_id && autosaved_commit_id+1 != commit_id) { if (commit_id == *c.commit_id+1 && redo_stack.empty() && saved_commit_id+1 != commit_id && autosaved_commit_id+1 != commit_id) {
// If only one line changed just modify it instead of copying the file // If only one line changed just modify it instead of copying the file
if (c.single_line) { if (c.single_line && c.single_line->Group() == AssEntryGroup::DIALOGUE) {
entryIter this_it = context->ass->Line.begin(), undo_it = undo_stack.back().file.Line.begin(); auto src_diag = static_cast<const AssDialogue *>(c.single_line);
while (&*this_it != c.single_line) { for (auto& diag : undo_stack.back().events) {
++this_it; if (diag.Id == src_diag->Id) {
++undo_it; diag = *src_diag;
break;
} }
undo_stack.back().file.Line.insert(undo_it, *c.single_line->Clone());
delete &*undo_it;
} }
else
undo_stack.back().file = *context->ass;
*c.commit_id = commit_id; *c.commit_id = commit_id;
return; return;
} }
undo_stack.pop_back();
}
redo_stack.clear(); redo_stack.clear();
undo_stack.emplace_back(*context->ass, c.message, commit_id); undo_stack.emplace_back(*context->ass, c.message, commit_id);
@ -305,28 +369,27 @@ void SubsController::OnCommit(AssFileCommit c) {
*c.commit_id = commit_id; *c.commit_id = commit_id;
} }
void SubsController::Undo() { void SubsController::ApplyUndo() {
if (undo_stack.size() <= 1) return; // Keep old lines alive until after the commit is complete
AssFile old;
old.swap(*context->ass);
redo_stack.splice(redo_stack.end(), undo_stack, std::prev(undo_stack.end())); *context->ass = undo_stack.back();
*context->ass = undo_stack.back().file;
commit_id = undo_stack.back().commit_id; commit_id = undo_stack.back().commit_id;
context->ass->Commit("", AssFile::COMMIT_NEW); context->ass->Commit("", AssFile::COMMIT_NEW);
} }
void SubsController::Undo() {
if (undo_stack.size() <= 1) return;
redo_stack.splice(redo_stack.end(), undo_stack, std::prev(undo_stack.end()));
ApplyUndo();
}
void SubsController::Redo() { void SubsController::Redo() {
if (redo_stack.empty()) return; if (redo_stack.empty()) return;
undo_stack.splice(undo_stack.end(), redo_stack, std::prev(redo_stack.end()));
context->ass->swap(redo_stack.back().file); ApplyUndo();
commit_id = redo_stack.back().commit_id;
undo_stack.emplace_back(*context->ass, redo_stack.back().undo_description, commit_id);
context->ass->Commit("", AssFile::COMMIT_NEW);
// Done after commit so that the old active line and selection stay alive
// while the commit is being processed
redo_stack.pop_back();
} }
wxString SubsController::GetUndoDescription() const { wxString SubsController::GetUndoDescription() const {

View file

@ -62,6 +62,9 @@ class SubsController {
/// Set the filename, updating things like the MRU and last used path /// Set the filename, updating things like the MRU and last used path
void SetFileName(agi::fs::path const& file); void SetFileName(agi::fs::path const& file);
/// Set the current file to the file on top of the undo stack
void ApplyUndo();
public: public:
SubsController(agi::Context *context); SubsController(agi::Context *context);

View file

@ -361,8 +361,7 @@ Vector2D VisualToolBase::GetLinePosition(AssDialogue *diag) {
if (Vector2D ret = vec_or_bad(find_tag(blocks, "\\move"), 0, 1)) return ret; if (Vector2D ret = vec_or_bad(find_tag(blocks, "\\move"), 0, 1)) return ret;
// Get default position // Get default position
int margin[3]; auto margin = diag->Margin;
memcpy(margin, diag->Margin, sizeof margin);
int align = 2; int align = 2;
if (AssStyle *style = c->ass->GetStyle(diag->Style)) { if (AssStyle *style = c->ass->GetStyle(diag->Style)) {