четверг, 31 марта 2011 г.

Медленный DataTable?!

Вроде бы, как же так, DataTable, специальный класс для работы с данными. Причем, с большими объемами данных. И все-таки, проблемы с производительностью могут возникнуть даже с ним!

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

Простой тест!

Создадим широкую таблицу, 100 колонок (типа String). Добавим в неё пустяковый, в общем-то, объем данных - 3000 строк. Пробежимся по всей таблице, и проставим всем ячейкам значение "test".

foreach (DataRow row in table.Rows)
  foreach (DataColumn column in table.Columns)
    row[column] = "test";

Замерим время выполнения...
00:00:01.7110978

Ого! Нехило, для такой, вроде бы простой операции!? 2 секунды на каких-то жалких 3000 строк!?

И заметьте, никакого извращенного кода. Вполне нормальный код, который мог бы написать любой из нас. Единственная особенность - широкая таблица, много колонок.

Вот и у нас, в одном из совершенно реальных проектов, обнаружилась широкая таблица на 70 колонок. И с ней возникла похожая проблема. Пришлось немало повозиться, чтобы локализовать эту проблему и понять, в чем дело. И еще полчаса на то, чтобы выяснить, как это исправить.

Решение оказалось довольно неожиданным. Оказывается, в случае широких таблиц, быстрее удалить целую строку и добавить новую (фактически пересоздав таблицу), чем заполнять строки стандартным способом.

Давайте проверим.
Для того, чтобы пример был более реалистичным, я забил таблицу случайными целочисленными значениями, и в дальнейшем использовал для тестирования следующий код:

Measure("Standard method", 1, () =>
{
    foreach (DataRow row in table.Rows)
        foreach (DataColumn column in table.Columns)
        {
            int value = Convert.ToInt32(row[column]);
            if (value > 100)
                row[column] = value * 2;
            else
                row[column] = value * 3;
        }
});

Ничего сложного. Если значение больше 100, умножаем его на 2, иначе на 3.
А теперь то же самое, но с пересозданием строк:

Measure("Row recreate method", 1, () =>
{
    for (int i = 0; i < tempTable.Rows.Count; i++)
    {
        List<object> values = new List<object>();
        foreach (DataColumn column in tempTable.Columns)
        {
            int value = Convert.ToInt32(tempTable.Rows[0][column]);
            if (value > 100)
                values.Add(value * 2);
            else
                values.Add(value * 3);
        }

        tempTable.Rows.RemoveAt(0);
        tempTable.Rows.Add(values.ToArray());
    }
});

Да уж! Выглядит похуже, а уж читается совсем плохо.
Но давайте потестируем производительность этих двух вариантов кода на таблицах различной конфигурации...

100 колонок, 3000 строк:
Standard method => 00:00:02.3351335
Row recreate method => 00:00:00.1030059


10 колонок, 30000 строк:
Standard method => 00:00:00.2970170
Row recreate method => 00:00:00.1980114


1 колонка, 300000 строк:
Standard method => 00:00:00.1740099
Row recreate method => 00:00:01.2810733


Результаты налицо: для большинства таблиц вариант с пересозданием строк будет работать очень быстро.

P. S.  Исходники метода Measure можно взять из статьи про производительность Lua Interface.

вторник, 29 марта 2011 г.

Интеграция SharePoint с внешними приложениями через SSIS

Стандартными средствами интеграции SharePoint-списков и сторонних приложений являются BCS (Business Connectivity Services, в 2007й версии - Business Data Catalog). Я думаю, большинство SharePoint-разработчиков не раз и не два имели дело с BCS. И вот по проcшествии некоторого времени, всегда оказывается, что для некоторых задач BCS не подходят! :(

Фактически, BCS без проблем позволяет решать только примитивные задачи, ну к примеру заCRUDить табличку SQL в список SharePoint. А чуть более сложные задачи требуют либо больших телодвижений, либо ну ОЧЕНЬ больших телодвижений... :(

Не вдаваясь в подробности, можно сказать, что BCS по-прежнему не идеален.

Антон Вишняков, за это ему отдельное спасибо, открыл мне глаза на еще один способ (помимо, конечно, "писать все руками") для интеграции данных SharePoint с внешними приложениями, который иногда может оказаться весьма полезным.

Способ заключается в использовании SQL Server Integration Services (SSIS) совместно с SharePoint List Source and Destination из OpenSource-проекта Microsoft SQL Server Community Samples: Integration Services.

Хочется немного рассказать об этом способе "на уровне теории", надеюсь эта информация будет полезна.

Те, кто видел DTS или работал уже с его обновленной версией - SSIS, наверняка представляют, что это такое. Если вкратце, SSIS позволяет задать источники данных, затем совершить над ними какие-нибудь преобразования, и конечный результат поместить в некий приемник данных.


Пример схемы преобразования данных в SSIS
В качестве преобразований данных могут выступать, к примеру:
  • Разделение и соединение данных - как вертикально (по столбцам), так и горизонтально (аналогично union all).
  • Сортировка
  • Группировка
  • Подведение итогов
  • Создание сводных таблиц (Pivot table)
  • Собственные виды преобразований

В качестве источника данных и приемника данных могут выступать, в простейших случаях, таблицы и базы данных MS SQL, но также это могут быть Excel-таблицы, соединения OLE DB и  ADO.Net, и т.д. Помимо встроенных источников и приемников, можно создавать собственные.

Источник и приемник для списков SharePoint в состав SSIS не входят, они были написаны отдельно и представляют собой решение Visual Studio, частично на C#, частично на VB.Net. Причем, это решение подойдет даже для 2007го SharePoint'а, поскольку не использует Client Object Model, а действуют через веб-сервис lists.asmx.

Следует отметить, что сделано все довольно дотошно. В частности, ребята постарались озаботиться такой немаловажной штукой, как производительность:
  • Источник данных списка SharePoint запрашивает не весь список, а только затребованный его объем. Выборку колонок можно ограничить через дизайнер, а также можно задавать произвольные CAML-запросы.
  • Есть настройка, которая позволяет задать количество строк "за этап". Для некоторых "широких" (с большим числом столбцов) списков такое вот блочное чтение позволяет увеличить итоговую производительность.
  • Приемник данных обновляет только нужные колонки и только нужные строки (через Batch API).
И все это - не забываем, Open Source. Есть возможность допилить.

Если решите попробовать, пример по созданию SSIS-проекта для интеграции с SharePoint описан на MSDN.

P.S. Сам пока не пробовал, так что это все пока только в теории, но думаю, этот способ вполне имеет шансы на успех. Надеюсь, будет возможность где-нибудь попробовать на рабочем проекте, если это удастся - я отпишусь! :)

понедельник, 21 марта 2011 г.

Локализация SharePoint: кодогенерация ресурсных классов

SharePoint обладает одной довольно неприятной особенностью в плане локализации: он плохо дружит со стандартными средствами ASP.Net в этой области. Фактически, в SharePoint-проектах желательно везде использовать локализацию через SPUtility.GetLocalizedString, причем в качестве параметра language нужно обязательно передавать свойство LCID объекта Thread.CurrentThread.CurrentUICulture (чтобы работала on-fly локализация).

Соответственно, даже обернув вызов GetLocalizedString в какие-то минимальные врапперы, все равно от нехороших конструкций вида LocalizationHelper.Localize("ResourceName", "ResourceFile") уйти не удастся. Основная проблема - это, конечно, наличие в таком коде большого количества "magic strings", со всеми вытекающими.

Частично от этого спасает введение класса, к примеру, ResourceNames, который хранит константы для всех названий ресурсов. Однако, в этом случае приходится содержать целый лишний класс, поддерживать его, да и от magic strings мы таким образом не избавляемся, а просто держим их отдельно.

Достаточно долгое время наш основной рабочий проект работал именно по приведенной выше схеме с magic strings. Причем, когда я (еще летом 2010) исследовал возможности и средства локализации проекта, естественно искал в интернетах - но никаких других толковых решений не нашел.

И вот один мой коллега реализовал другое решение - на основе кодогенерации. С кодогенерацией я работал очень мало, по сути только полгода назад узнал о том, что это такое. Но идея-то очень логичная, и совершенно изумительная в плане результата: полностью избавляемся от magic strings, и получаем все преимущества интеллисенса.

В результате применения кодогенерации, становится возможным использовать (причем, Intellisense при этом отлично работает) конструкции вида: Resources.ResourceFileName.ResourceName. Для нашего основного проекта я сделал еще интереснее. Поскольку ресурсов там очень много, и все именуются по стандартному принципу, мне удалось сделать бОльшую вложенность классов, и за счет этого упростить выбор ресурса. Т.е. у нас получилось что-то вроде такого: Resources.ResourceFileName.ProjectName.ModuleName.ResourceName.

Более того, мне также удалось добиться отображения комментариев (колонка Comment в файле resx) через Intellisense! Выглядит все это весьма эффектно:


В общем, в этом посте я расскажу про особенности реализации, те же кому важен конечный результат, могут стянуть сразу tt-файлик (в конце поста).

среда, 16 марта 2011 г.

Sandcastle, MAML, и документирование ПО

Как известно, Visual Studio позволяет писать комментарии в коде в xml-формате, если они предваряются тройным слешем ("///"). Большинство при этом используют лишь пару простых тегов, как например <summary> и <param>.

На самом деле, тегов там можно использовать довольно много. Наиболее полезные, на которые я б сразу посоветовал обратить внимание - это <see cref="">, <example>, <para>, и <remarks>. Также, иногда очень полезен будет тег <list>. Более подробно обо всем этом ниже.

Оказывается, эти комментарии видны не только в интеллисенсе, но также на их основе можно сгенерировать CHM-файл или справочный веб-сайт, с помощью специально предназначенных для этого, общедоступных инструментов. Внешний вид получается красивый, и главное очень всем известный - всем, кто хоть раз бывал на MSDN :) Пользоваться такой справкой довольно удобно.


Мне пришлось разобраться со всем этим, когда разрабатывал описание API для одного из наших клиентов. И настолько мне понравился результат, что по собственной инициативе задокументировал некоторые неймспейсы основного нашего проекта, и сгенерировал по ним chm-файлик (он как раз представлен на скриншоте выше). Коллегам эта затея пришлась по душе, так что надеюсь в дальнейшем общими усилиями нам удастся эту линию продолжить, и создать документацию для большей части общих сборок.

В общем, в этом сообщении я расскажу немного про язык MAML, про инструменты для генерации документации (особенно про SandCastle), ну и вообще про то, как это все настраивать и как с этим всем работать.