Performanceoptimierung bei Social Media Integration
Wer Social Medias auf seiner Webseite integriert, stolpert als erste Hürde über die eigenen APIs der Social Medias, die sich jedoch leicht überwinden lässt wie mein letzter Post zeigt. Die zweite Hürde ist umso grösser: Die Performance der Zugriffe ist denkbar schlecht. Der Flaschenhals ist dabei das Social Media selbst. Facebook schneidet mit Abstand am schlechtesten ab. Selbst wenn die Abfrage stark eingeschränkt wird, kann sie immer noch bis zu 2 Sekunden dauern. Bei Youtube sind die Werte deutlich besser und liegen bei der ersten Abfrage für gewöhnlich unter einer Sekunde und bei einer wiederholten sogar unter einer halben.
Wenn der Channel asynchron geladen wird, ist das alles kein Problem. Wer jedoch auf seiner Webseite kein Busy Gif anstelle der Facebook Posts sehen will, sollte einen anderen Weg gehen. Ich möchte eine Caching Lösung vorstellen, an der ich mitgearbeitet habe. Das Prinzip ist denkbar einfach: Alle Social Media Channels werden jeweils in einem separaten Thread alle 2 Minuten aktualisiert. Die Webseite selbst zeigt die aktuellen Posts immer sofort an und hat so maximal einen veralteten Stand von 2 Minuten. Die Zeit lässt sich natürlich variieren. Der Vorteil liegt auf der Hand: Der Benutzer sieht die gewünschten Daten sofort und merkt nichts davon, dass sie geladen werden müssen.
// Base class for social media data providers public abstract class SocialMediaDataProvider { public SocialMediaDataProvider(string channelName, int n) { ChannelName = channelName; N = n; } // Data Providers are equal if their channel names are equal public override bool Equals(object obj) { if (obj is SocialMediaDataProvider) { if (obj != null) { return this.ChannelName.Equals(((SocialMediaDataProvider)obj).ChannelName); } } return base.Equals(obj); } public override int GetHashCode() { return base.GetHashCode(); } public abstract ListGetTopNPostings(); public int N { get; private set; } public string ChannelName { get; private set; } }
Davon abgeleitet gibt es je einen Provider für bspw. Facebook, Twitter und Youtube, der die Daten entsprechend lädt. Eine Factory Klasse erstellt anhand einer ID den entsprechenden Subtyp.
// Factory for social media data providers public static class SocialMediaDataProviderFactory { public static SocialMediaDataProvider GetSocialMediaDataProvider(string socialID, string channelName, int n) { if (socialID == Constants.FACEBOOK) { return new FacebookDataProvider(channelName, n); } else if (socialID == Constants.TWITTER) { return new Linq2TwitterDataProvider(channelName, n); } else if (socialID == Constants.YOUTUBE) { return new YouTubeDataProvider(channelName, n); } return null; } }
Der eigentliche Cache funktioniert über einen Timer und lädt die Daten im Hintergrund regelmässig neu, so dass sie jederzeit abgerufen werden können. Der Zugriff über
GetLatestPostings
verursacht keine Verzögerung, wenn der Channel bereits vorgeladen wurde (beim Applikationsstart). Handelt es sich jedoch um einen neuen Channel, müssen die Daten beim ersten Mal in Echtzeit geladen werden.
// Holds the latest social media data and reloads them persistently public class SocialMediaDataHolder { private static object _lock = new object(); private const int DELAY_MILLISECONDS = 120000; private static bool _isStarted = false; private static Timer _timer = null; private static List_dataProviders = new List (); private static Dictionary > _latestPostings = new Dictionary >(); public static void Start() { lock (_lock) { if (!_isStarted) { _isStarted = true; _timer = new Timer(TimerCallback, null, 0, DELAY_MILLISECONDS); } } } public static void Stop() { lock (_lock) { if (_isStarted) { _timer.Dispose(); } _isStarted = false; } } public static void AddProvider(ID socialId, string channelName, int n) { var provider = SocialMediaDataProviderFactory.GetSocialMediaDataProvider(socialId, channelName, n); AddProvider(provider); } public static void AddProvider(SocialMediaDataProvider provider) { if (provider == null) { return; } lock (_lock) { if (!_dataProviders.Contains(provider)) { _dataProviders.Add(provider); } } } public static void ClearProviders() { lock (_lock) { _dataProviders.Clear(); _latestPostings.Clear(); } } public static void RemoveProvider(string channelName) { var provider = _dataProviders.Where(p => p.ChannelName.Equals(channelName)).FirstOrDefault(); RemoveProvider(provider); } public static void RemoveProvider(SocialMediaDataProvider provider) { if (provider == null) { return; } lock (_lock) { if (_dataProviders.Contains(provider)) { _dataProviders.Remove(provider); } if (_latestPostings.ContainsKey(provider.ChannelName)) { _latestPostings.Remove(provider.ChannelName); } } } public static List GetLatestPostings(ID socialId, string channelName, int n) { // provider added on runtime, load data first if (!_latestPostings.ContainsKey(channelName)) { // add new provider var provider = SocialMediaDataProviderFactory.GetSocialMediaDataProvider(socialId, channelName, n); AddProvider(provider); try { List newPostings = provider.GetTopNPostings(); _latestPostings.Add(channelName, newPostings); } catch (Exception exc) { // do not touch cached list -> return empty list return new List (); } } return _latestPostings[channelName]; } private static void TimerCallback(object state) { foreach (var provider in _dataProviders) { List newPostings = null; try { newPostings = provider.GetTopNPostings(); if ((newPostings.Count == 1) && (newPostings[0] is PostingError)) { continue; } } catch (Exception exc) { //Do nothing - do not touch cached list } if (newPostings != null) { if (_latestPostings.ContainsKey(provider.ChannelName)) { _latestPostings[provider.ChannelName] = newPostings; } else { _latestPostings.Add(provider.ChannelName, newPostings); } } } } }
Der DataHolder kann bereits beim Applikationsstart (im Global.asax) angestossen werden, wenn die zu ladenden Channels bekannt sind. Andernfalls können neue Channels auch zur Laufzeit hinzugeladen werden, wobei es sich empfiehlt möglichst alle Channels schon beim Applikationsstart zu laden.
// Global.asax public void Application_Start() { // Start loading 5 social media posts from each facebook and twitter channel repeatedly SocialMediaDataHolder.AddProvider(Constants.FACEBOOK, "MyFacebookChannel", 5); SocialMediaDataHolder.AddProvider(Constants.TWITTER, "MyTwitterChannel", 5); SocialMediaDataHolder.Start(); }