VirtualStringTree правильное / рекомендуемое использование

#performance #delphi #virtualtreeview #tvirtualstringtree

#Производительность #delphi #virtualtreeview #tvirtualstringtree

Вопрос:

Я уже некоторое время использую virtualstringtree. Я использую его для двух разных целей: во-первых, как обычное дерево для выбора, отображения данных, а во-вторых, как сетку для отображения выходных данных из операторов SQL.

Все мои данные, загруженные в деревья, взяты из базы данных. Для примера дерева у меня есть поле ParentID для различения иерархии, а для примеров сетки я просто использую инструкцию SQL с настраиваемой записью для каждого дерева (которая уникальна).

Мои вопросы касаются предпочтительного / наилучшего способа заполнения дерева. Я прочитал из документов VST, что вы должны использовать событие onInitNode вместе с rootnodecount . Однако я обнаружил, что использование метода addChild() очень похоже, хотя это и не поощряется.

Позвольте мне показать несколько (упрощенных) примеров:

1. Иерархия

 type PData = ^rData;
    rData = packed record
      ID : Integer;
      ParentID : Integer;
      Text : WideString;
    end;

procedure Loadtree;
 var Node : PVirtualNode;
Data : PData;
 begin
    Q1 := TQuery.Create(Self);
            try
                Q1.SQL.Add('SELECT * FROM Table');
            Q1.Open;
            Q1.Filter := 'ParentID = -1'; //to get the root nodes
            Q1.Filtered := True;
            while not Q1.Eof do
            begin
                    Node := VST.AddChild(nil);
                    Data := VST.GetNodeData(Node);
                    Data.ID := Q1.Fields[fldID].AsInteger;
                    Data.ParentID := Q1.Fields[fldParentID].AsInteger;
                    Data.Text := Q1.Fields[fldText].AsString;
                    //now filter the query again to get the children of this node
                    PopulateChildren(Data.ParentID,Node); //add children to this node and do it recursively
                    Q1.Next;
            end;
       finally
          Q1.free;
      end;

end;
  

2. Сетка

 procedure LoadGrid;
var Node : PVirtualNode;
Data : PData;
begin
     Q1 := TQuery.Create(self);
        try
            Q1.SQL.Add('SELECT * FROM Table');
            Q1.Open;
            while not Q1.eof do
            begin
                    Node := VST.AddChild(nil);
                    Data.ID := Q1.Fields[fldID].AsInteger;
                    Data.Text := Q1.Fields[fldText].AsString;
                    Q1.Next;
            end;
    finally
            Q1.Free;
    end;
end;
  

Итак, по сути, я обхожу методы / свойства RootNodeCount и OnInitNode и использую старомодный способ добавления узлов в дерево. Кажется, все работает нормально. Обратите внимание, что в примере я создаю и уничтожаю свои запросы во время выполнения.

Причина, по которой я начал использовать дерево таким образом, заключается в том, что я мог загружать все данные в дереве один раз, а затем освобождать TQuery после того, как я закончил его использовать. Я думал, что независимо от сохранения / создания TQuery, мне все равно нужно будет использовать мою запись rData для хранения данных, следовательно, используя больше памяти, если я не уничтожу TQuery. В настоящее время мое приложение использует около 250 МБ при полной загрузке и может увеличиться, когда я запускаю отчеты SQL и отображаю их в VST. Я видел, что он использует около 1 ГБ оперативной памяти, когда я запускал отчет SQL с более чем 20000 узлами и более чем 50 столбцами. Что я хотел бы знать, если способ, которым я использую VST, неверен в отношении минимизации использования памяти?

Было бы лучше создать запрос для времени жизни дерева и использовать событие onInitNode? Так что, когда данные запрашиваются деревом, оно извлекает их из TQuery, используя событие onInitNode / OnInitChildren (т.Е. Чисто виртуальную парадигму дерева)? Таким образом, мне нужно было бы поддерживать TQuery в течение всего срока действия формы. Будет ли какая-либо выгода от памяти / производительности при использовании этого способа?

В приведенных выше ситуациях я вижу, что разница для примера сетки будет намного меньше (если таковая имеется), чем иерархия — из-за того, что все узлы должны быть инициализированы при заполнении.

Комментарии:

1. Одна из основных причин, по которой вы используете так много памяти, заключается в том, что вы копируете данные на каждый узел; если вы просто сохраняете только ссылку (например, идентификатор (целое число в вашем примере), вы можете искать правильную запись в наборе записей в событиях OnGetText и т. Д.). Таким образом, вы сэкономите много памяти и будете придерживаться виртуальной парадигмы.

2. Будет ли это так, если вы считаете, что я освобождаю набор записей после того, как я закончил с ним? Поэтому, чтобы придерживаться виртуальной парадигмы, мне нужно было бы всегда иметь под рукой набор записей. Мой текущий метод я завершаю и освобождаю его после заполнения дерева. А как насчет производительности Locate, который мне нужно будет использовать для поиска записи в наборе записей?

3. Я лично использую набор данных памяти для хранения данных в памяти, что очень эффективно для строк (никаких отдельных строковых объектов для каждой ячейки; накладные расходы на строки существенны для коротких строк (например, заголовок memmanager, заголовок строки и нулевой терминатор)). Производительность идеальна; обычно при рисовании сетки требуется менее 1% дополнительной мощности процессора.

Ответ №1:

Причина AddChild() , по которой это не рекомендуется, заключается в том, что это нарушает виртуальную парадигму компонента — вы создаете все узлы, даже если они вам могут никогда не понадобиться. Допустим, запрос возвращает 100 записей, но в дереве одновременно отображается 10 узлов. Теперь, если пользователь никогда не прокручивает вниз, вы тратите ресурсы на 90 узлов, поскольку они никогда не нужны (не становятся видимыми). Так что, если вы беспокоитесь об использовании памяти, AddChild() это плохая идея. Другое дело, что если результирующий набор большой, заполнение дерева требует времени, и ваше приложение в это время не реагирует. При использовании виртуального способа ( RootNodeCount и OnInitNode ) дерево «готово» немедленно, и пользователь не испытывает никаких задержек.

Опять же, в случае (относительно) небольших наборов результатов использование AddChild() может быть лучшим вариантом — это позволит вам загружать данные за одну короткую транзакцию. Т.Е. в случае древовидной структуры имело бы смысл загружать весь «уровень» сразу, когда пользователь расширяет родительский узел.

Использование DB с VT немного сложно, поскольку запрос к БД тоже является специальным ресурсом со своими собственными ограничениями (вы хотите сократить транзакции, это относительно медленно, БД может поддерживать только односторонний курсор и т. Д.).
Поэтому я бы сказал, что это то, что вы должны решать в каждом отдельном случае, в зависимости от варианта использования и объема данных, т.Е.

  • небольшой результирующий набор можно загружать сразу с AddChild() внутренней структурой данных или во внутреннюю структуру данных, а затем использовать его в событиях VT;
  • загрузка одного уровня за раз, вероятно, является хорошим компромиссом для деревьев;
  • в случае очень больших наборов результатов загрузка пакетами может привести к снижению производительности и использования памяти, но усложняет код, управляющий VT.

Комментарии:

1. 1 спасибо за ответ. Кажется, что большинство моих деревьев должно быть в порядке, поскольку они загружают только <100 узлов из БД, и я бы предпочел иметь меньшие запросы, а не более мелкие. Однако есть несколько мест, где набор результатов запроса больше. Возможно, я рассмотрю возможность изменения в этих обстоятельствах.

2. просто попробовал использовать rootnodecount и TQuery. Не так просто, как я думал, поскольку TQuery не предоставляет произвольный доступ к записям, как это делает массив. Похоже, я буду придерживаться того, что у меня есть на данный момент

3. У меня есть пользовательский элемент управления, подобный TQuery, который я написал, который запрашивает базу данных и сохраняет набор записей в памяти. Я попытался использовать OnInit и, похоже, не могу заставить его работать. При отслеживании вызовов OnInit и GetText я заметил, что OnInit вызывается 2 раза, а затем GetText. В моем OnInit мой элемент управления, подобный TQuery, поддерживает -> Next(); поэтому при каждом вызове OnInit я заполняю VST Data * данными и вызываю -> Next() . Я получаю дерево повторяющейся информации. Есть ли какие-либо рабочие примеры C , которые вы тоже можете указать? В документации говорится об OnInit для изменения размера ячеек, и большинство материалов написано на obj-Pascal.

Ответ №2:

TQuery предоставляет доступ к записям, подобный массиву.

Если вы сверитесь с документацией, вы можете перейти к каждой записи, используя RecNo свойство, и вы можете перейти к каждому полю, используя Fields[i] (по его индексу).

Нет никаких препятствий для использования TVirtualStringTree в качестве виртуального режима подключения к базе данных.

После выполнения запроса данные в любом случае доступны в памяти, TDataSet так почему бы не использовать их в качестве контейнера данных?

PS: доступ к полям с использованием их индекса быстрее, чем FieldByName .