Ниже я постараюсь описать некоторые причины медлительности при работе с SharePoint, и изложить рекомендации о том, как решить проблемы, связанные с производительностью.
SPSite и SPWeb
1. Старайтесь вообще не создавать эти объекты без надобности. Это весьма долгий процесс (насколько я помню, аж целых 0.2 секунды). Самое смешное, когда пишут что-нибудь такое:
foreach (MyObject myObject in MyObjectCollection) { using (SPSite site = new SPSite("http://localhost")) { // тут что-нибудь делаем } }Т.е. SPSite создается и затем освобождается в цикле. Естественно, цикл нужно внести внутрь блока using. Но ведь мы не привыкли задумываться о таких вещах в ASP.Net!
Да что там говорить! Даже очень опытные программисты, которые уже знают об этой тонкости, часто совершают эту ошибку. К примеру, у вас есть некая функция, которая осуществляет действия в контексте учетки пула (через SPSecurity.RunWithElevatedPrivileges). Эта функция, что совершенно логично, запрятана где-нибудь глубоко в классе, условно назовем, BusinessLogic... Естественно, чтобы использовать RunWithElevatedPrivileges, нужно создать сайт и веб. И вот, в один прекрасный момент вы начинаете вызывать вашу собственную функцию в цикле (скорее всего, уже даже не помня, что в ней вообще есть вызов RunWithEvaluatedPrivileges)...
foreach (SPListItem listItem in list.Items) { // что-нибудь делаем, и в конце... BusinessLogic.ChangeItemPermissions(list.ID, listItem.UniqueId); } // ... // где-то в классе BusinessLogic public static void ChangeItemPermissions(Guid listID, Guid listItemID) { SPSecurity.RunWithEvaluatedPrivileges(delegate() { using (SPSite site = new SPSite(siteUrl)) { using (SPWeb web = site.OpenWeb()) { SPList list = web.Lists[listID]; SPListItem item = list.Items[listItemID]; // ну и дальше выставляем привилегии... } } }); }И вы еще потом удивляетесь: "Откуда же такие тормоза?" ?! :)
2. Не забывайте, что обращение к свойству экземпляра SPSite AllWebs равносильно созданию Web'а. Есть и другие подобные свойства. К примеру, совсем недавно я был очень удивлен, когда локализовал почти полусекундные тормоза в следующем фрагменте кода:
SPWeb rootWeb = contextParams.Web.Site.RootWeb;
Кто бы мог подумать, что даже если contextParams.Web уже является корневым узлом, то этот код всё равно открывает этот узел еще раз!3. Всё, что сами создаете - нужно диспозить. Это очень тонкий момент, по нему написано множество руководств. Тонкий, потому что иногда диспозить нельзя. К примеру, нельзя диспозить RootWeb, он диспозится вместе с сайтом. Более банальный пример - нельзя диспозить общие объекты, такие как SPContext.Current.Site и SPContext.Current.Web, SharePoint делает это сам после Render'а.
Если вы что-то не высвободили - в случае циклов, это чаще всего огромные потери памяти. Т.е. после первого десятка-двух итераций цикл начинает выполняться всё меееедленнее и мееедленнееее....
В лог сыпятся ошибки "An SPRequest object was not disposed before the end of this thread. " и подобные. На самом деле довольно легко не заметить такие вещи, особенно благодаря наличию хитрых свойств, как упомянуто в п.2. Чтобы с этим бороться, можно, помимо аккуратного кодинга, использовать специальную утилиту от Microsoft - SPDisposeCheck, которая позволяет анализировать уже откомпилированные dll-ки, что весьма удобно.
Между прочим, в моей, уже весьма отутюженной сборке, эта штука с первого раза нашла 4 дырки (правда, одна впоследствии оказалась ложным срабатыванием):
Авторы пишут, что есть ложные срабатывания, да и не все дырки она находит. Но ведь всё равно, согласитесь, очень хорошая утилита для дополнительной проверки. Причем, благодаря командной строке, процесс проверки легко автоматизировать, используя соответствующий BuildAction.
Поскольку эта утилита - не панацея, то всем, кто еще не видел, будет ОЧЕНЬ полезно посмотреть на MSDN статейку с шаблонами для программирования многих распространенных ситуаций, и использовать эти шаблоны в дальнейшем. Пишите аккуратно, и потом, глядишь, переписывать не придется (как пришлось мне :(... )
SPList
Почти всё, что было сказано про SPWeb и SPSite, относится и к SPList. Его не нужно диспозить, но в остальном, этот объект и его внутренности обрабатываются весьма долго, и в циклах это может стать опасным. Чаще всего я стараюсь не использовать объекты SPList напрямую, скрывая их в DataLayer-е. Для этого нужно создать модель в виде обычного класса (не используйте для этого SPListItem, ведь это тоже великий и ужасный SharePoint-объект!), и все действия с моделью выполнять через контроллер, в который крайне желательно ввести функции кэширования.
Давайте возьмем для примера список продуктов, включающий три банальных колонки:
public class Product { public string Name {get; set;} public double Price {get; set;} public int Quantity {get; set;} }Создадим для этого класса кэширующий контроллер-синглтон:
public class ProductController { const string CacheKey = "ProductController"; public static ProductController Instance { get { if (HttpRuntime.Cache[CacheKey] == null) { HttpRuntime.Cache.Insert(CacheKey, new ProductController(), DateTime.Now.AddSeconds(5), Cache.NoSlidingExpiration); } return (ProductController)HttpRuntime.Cache[CacheKey]; } } // тут будет остальной код }Из фрагмента видно, что класс-контроллер будет кэшироваться каждые 5 секунд, т.е. изменения в списке будут в нем отображаться практически мгновенно. Давайте посмотрим, как же будут храниться и обрабатываться элементы списка. Вместо заглушки, вставляем следующий код:
const ListName = "ProductsList"; private List<Product> Products = null; public IEnumerable<Product> GetAllProducts() { if (Products == null) { Products = new List<Product>(); SPWeb web = SharePointHelper.GetRootWeb(); SPList list = web.Lists[ListName]; foreach (SPListItem item in list.Items) { Product newProduct = new Product(); newProduct.Name = item["Name"]; newProduct.Price = item["Price"]; newProduct.Quantity = item["Quantity"]; Products.Add(newProduct); } } return Products; }Если вам требуется помимо простого чтения, еще изменять продукты, или еще что-то с ними делать - добавьте соответствующие методы (например, void Save(Product product), и т.п.). В итоге, и с производительностью проблем не будет - за счет кэша, да и понятнее гораздо стало, по фэншую ака MVC :).
Что еще
Практически любые SharePoint-объекты следует использовать очень аккуратно. Первым номером, если говорить о MOSS, идет, бесспорно, BDC (Business Data Catalog). Кэшировать его! Хотя бы на пару секунд - чтобы успеть обработать постбэк. И не забывайте, что кэш должен быть зависимым от пользователя, особенно если вы используете какие-то собственные правила для ограничения доступа.
Другой пример из MOSS: в свое время мне требовалось сделать пользователе-зависимое хранение данных. Т.е., у каждого пользователя - своя настройка. Сделал через MOSS-овский UserProfileManager.
Совсем недавно обнаружил, что некий простейший цикл по добавлению колонок к SPGridView выполняется чудовищно медленно. Оказалось, в цикле фигурировало то самое свойство, которое так удачно было запрятано, что я и думать о нем забыл. Вывод: всё что шарепойнтовское прячете, кэшируйте как следует!
Удачей, и до новых встреч!
P.S. Код может не скомпилироваться, писал на ходу.
"Из фрагмента видно, что класс-контроллер будет кэшироваться каждые 5 секунд"
ОтветитьУдалитьА мне показалось что из фрагмента следует, что через пять секунд кэш сбросится и при следующем обращении будет снова чтение информации. Никакого автоматического кэширования я не вижу. Ну или ткните где оно?
имеется в виду, что информация будет обновляться с периодичностью не быстрее, чем раз в 5 секунд
ОтветитьУдалитьесли же идет много-много запросов в течение нескольких секунд, как это бывает в случае пиковых нагрузок, то система будет отдавать пользователям кэш, что ускорит отклик на 1-2 порядка