Quantcast

comments edit

Le montage

Une Printrbot LC V2, en kit, vous la recevrez sous cette forme :

Le kit PrintrBot à l'arrivée

Un ensemble de sachets, de vis, de pièces en bois découpées au laser. Comptez au moins 6h pour le montage. La documentation indique 4h, mais c’est pour quelqu’un qui conçoit et qui monte des imprimantes 3D toute la journée.

Lors du montage, il s’agira de bien serrer toutes les vis, sans casser le bois, pour qu’il n’y ait aucun jeu entre les pièces fixes. Attention aux axes X, ne pas les verrouiller avant d’avoir bien vérifié que le charriot coulisse bien.

Pour les courroies, il faut les tendre suffisamment pour qu’elles produisent une note grave quand on les pince comme une corde de guitare. Pas trop de tension non plus, sinon vous allez endommager l’axe du moteur ou la courroie !

Premier lancement

Avant de brancher la machine, pensez a bien vérifier que les vis des endstop X Y et Z sont bien réglées. Si elles n’activent pas les interrupteurs lorsque le charriot ou le plateau arrive en butée, vous risquez d’endommager la machine.

Le plateau

La chose la plus importante pour une impression réussie est un plateau bien plan. Une plaque de verre (0.4mm d’épaisseur) aux bonnes dimensions, et fixé avec des pinces fera l’affaire :

Le plateau d'impression

Dans le cas de la PrintrBot LC V2, j’ai du couper les pinces, sinon le charriot butait dedans :

Détail sur les pinces

Pour les fixer, des pinces a circlip seront d’une grande aide:

Deux pinces à circlip

Maintenant, il va falloir régler la hauteur de chaque coin du plateau pour que la tête d’impression soit toujours a la même hauteur.

Pour cela, j’utilise des jauges d’épaisseur, et je règle la hauteur sur 0.15mm. Histoire de vous éviter des surprises, il vaut mieux faire ce réglage avec l’extrudeur et le plateau a température : 180° et 60° respectivement, pour du PLA.

Pourquoi 0.15 ? Parce qu’a cette épaisseur, la jauge est assez souple. Ensuite, dans slic3r, vous avez une option pour “baisser” le z. En effet, slic3r considère le z calibré a 0. Donc une des premières choses de faites dans le GCode est de placer la buse a la valeur z correspondant au layer height, par défaut 0.30. Donc dans notre cas, le z se trouvera a 0.45 mm du plateau au début de l’impression. Bien trop haut. Je l’ai compensé par un adjust z de -0.15. Donc la tête se trouvera a z = 0.30 mm du plateau.

comments edit

La dernière ligne droite dans l’implémentation de ce Serializer était d’avoir des performances similaires (ou meilleures!) à celles du BinaryFormatter du Framework .Net. En utilisant uniquement la reflection, ce n’était pas gagné d’avance.

foreach (var prop in type.GetFields(BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Public).OrderBy(x => x.Name))
{
    var v = this.DeserializeBase(prop.FieldType, destination, source);
 
    prop.SetValue(destination, v);
}

En effet, a chaque serialisation de la meme classe, ce code va récupérer les champs, encore et encore, ce qui n’est de loin pas efficace.

L’optimisation que j’ai choisi a été de remplacer l’utilisation de la reflection par la création de LambdaExpression basées sur les champs des objets a sérialiser.

gettersForType = new List<Tuple<Type, Func<object, object>>>();
 
// create getter list from fields
foreach (var prop in sourceType.GetFields(BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Public))
{
    var getters = CreateGetter(prop);
 
    gettersForType.Add(Tuple.Create(prop.FieldType, getters));
}

Ici, la reflection sera utilisée une fois pour obtenir la liste des champs, et générer les Getters qui permettront d’en lire les valeurs.

La méthode CreateGetter va retourner un Func<object, object> après avoir crée et compilé une LambdaExpression.

private static Func<object, object> CreateGetter(FieldInfo field)
{
    var fieldType = field.DeclaringType;
 
    // the input parameter for the lambda
    var sourceParameter = Expression.Parameter(typeof(object), "source");
 
    // as the parameter is of object type, a cast or conversion may be required
    var castedSource = GetExpressionAsTypeIfNeeded(sourceParameter, fieldType);
 
    // get field value
    var fieldExpression = Expression.Field(castedSource, field);
 
    // as the return parameter is of type object, a cast or conversion may be required
    var castedField = Expression.TypeAs(fieldExpression, typeof(object));
 
    // the lambda expression for accessing a field on an object
    var expr = Expression.Lambda<Func<object, object>>(castedField, sourceParameter);
    return expr.Compile();
}

Les éléments clés dans cette méthode sont :

  • La déclaration d’un paramètre pour la LambdaExpression, de type object, qui sera casté ou converti dans le bon type. En effet, comme on ne connait pas en avance le type de l’objet que l’on va désérialiser, la signature de la lambda expression est Func<object,object>.
  • La récupération d’une expression renvoyant la valeur du champ
  • Le cast de cette valeur en object pour la renvoyer.

De la même manière, j’avais implémenté une méthode CreateSetter, qui elle renvoyait un Action<object, object>. Cette méthode, créée a la volée pour un champ donné, prenait en paramètre l’objet en cours de désérialisation, ainsi que la valeur à assigner au champ.

private static Action<object, object> CreateSetter(FieldInfo field)
{
    var fieldType = field.DeclaringType;
 
    // the input parameter for the lambda
    var destinationParameter = Expression.Parameter(typeof(object), "destination");
    var valueParameter = Expression.Parameter(typeof(object), "fieldValue");
 
    var castedDestination = GetExpressionAsTypeIfNeeded(destinationParameter, fieldType);
    var castedValue = GetExpressionAsTypeIfNeeded(valueParameter, field.FieldType);
 
    // get field value
    var fieldExpression = Expression.Field(castedDestination, field);
 
    // as the return parameter is of type object, a cast or conversion may be required
    var assign = Expression.Assign(fieldExpression, castedValue);
 
    // the lambda expression for accessing a field on an object
    var expr = Expression.Lambda<Action<object, object>>(assign, destinationParameter, valueParameter);
    return expr.Compile();
}

Le souci de cette méthode est quelle renvoie un Action<object,object>. Or si les types valeurs “basiques” comme int, long, float, ne posent pas de souci, comme ils sont gérés par un sérializer a part, les structs, qui sont également passés par valeurs, ne sont pas désérialisés !

En effet, la méthode va agir sur une copie de l’instance en cours de désérialisation, dont les champs garderont les valeurs par défaut.

J’ai donc choisi d’implémenter une méthode plus complète, qui va non seulement générer le code pour définir les valeurs des champs, mais également prendre en charge la création de l’instance qui va être désérialisée. Cette méthode renvoie une fonction, qui prend en paramètre le flux dans lequel lire les valeurs a désérialiser, l’instance de l’objet sur laquelle seront définies les champs, et qui retourne l’instance désérialisée.

Dans le cas des types valeurs, la fonction générée se charge de la création de l’instance, et la retourne. De ce fait, on ne souffre plus des problèmes posés par l’implémentation précédente.

private Func<ExtendedBinaryReader, object, object> CreateSetters(Type type)
{
    // the input parameters of the generated lambda : the destination instance on which the setters will be applied
    var destinationParameter = Expression.Parameter(typeof(object), "destination");
 
    // the BinaryReader from which to get the data
    var binaryReaderParameter = Expression.Parameter(typeof(ExtendedBinaryReader), "source");
 
    // a variable to hold the destination instance
    var deserializedType = Expression.Variable(type, "destination");
 
    var expressionBlock = new List<Expression>();
 
    if (!type.IsValueType)
    {
        // if the type is not a value type the instance given as a parameter is used, or a new instance is created
        var coalesce = Expression.Coalesce(GetExpressionAsTypeIfNeeded(destinationParameter, type), Expression.New(type));
 
        // the first "line" of the lambda is to assign the destination variable
        expressionBlock.Add(Expression.Assign(deserializedType, coalesce));
    }
    else
    {
        // for a value type, a "new" instance is created
        expressionBlock.Add(Expression.Assign(deserializedType, Expression.New(type)));
    }
 
    var thisAsMethodTarget = Expression.Constant(this);
 
    var methodToCall = typeof(FastDefaultObjectSerializer).GetMethod("DeserializeBase");
    var deserializedTypeAsObject = Expression.TypeAs(deserializedType, typeof(object));
 
    foreach (var field in type.GetFields(BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Public))
    {
        // access to the field on the instance being deserialized
        var fieldExp = Expression.Field(deserializedType, field);
 
        var fieldType = Expression.Constant(field.FieldType);
 
        // a methood call expression
        var call = Expression.Call(
            thisAsMethodTarget,
            methodToCall,
            fieldType,
            deserializedTypeAsObject,
            binaryReaderParameter);
 
        // the result of the method call is converted to the field type if needed ...
        var callResultAsFieldType = GetExpressionAsTypeIfNeeded(call, field.FieldType);
 
        // ... and is assigned to the field
        var assignToField = Expression.Assign(fieldExp, callResultAsFieldType);
 
        expressionBlock.Add(assignToField);
    }
 
    // the return part of the lambda
    var returnTarget = Expression.Label(typeof(object));
    var returnExpression = Expression.Return(returnTarget, deserializedTypeAsObject, typeof(object));
    var returnLabel = Expression.Label(returnTarget, deserializedTypeAsObject);
 
    expressionBlock.Add(returnExpression);
    expressionBlock.Add(returnLabel);
 
    var block = Expression.Block(new ParameterExpression[] { deserializedType }, expressionBlock);
 
    var lambda = Expression.Lambda<Func<ExtendedBinaryReader, object, object>>(block, binaryReaderParameter, destinationParameter);
 
    return lambda.Compile();
}

Dans cette méthode, les points clés sont :

  • le test sur le type de l’objet a désérialiser : si il s’agit d’un type valeur, la méthode va le créer, et le retourner. Si non, la méthode utilisera l’instance passée en paramètre. l’appel a la méthode DeserializeBase, afin de s’appuyer sur les mécanismes implémentés précédemment pour la désérialisation
  • la génération d’un bloc de code pour la désérialisation de chaque champ

Cette optimisation a permi de diviser par 3 le temps de serialisation/deserialisation (10000 iterations) d’une classe simple.

Comme toujours, le code est disponible sur github

comments edit

Lorsque l’on ouvre le gestionnaire des taches Windows, on peut y voir un onglet “Mémoire (jeu de travail privé)” pour chaque processus. Mais que ce cache derrière ce chiffre ? Afin de le savoir, je vais vous présenter l’outil VMMap, de Sysinternals, disponible au téléchargement sur Technet.

Cet outil va vous présenter trois graphes de mémoires :

  • Commited : représente la quantité qu’occuperaient tout le code et données de l’application, ainsi que les fichiers mappés par celle ci.
  • Private Bytes : représente la quantité de mémoire demandée par le processus, et ne pouvant être partagée avec d’autres processus. Cette mémoire peut se trouver sur un fichier d’échange.
  • Working Set : représente la mémoire physique utilisée par le processus, c’est à dire qu’aucun accès au fichier d’echange ne sera fait lors d’un accès à cette mémoire.

Ces trois graphes sont subdivisés en différentes catégories. Typiquement, les catégories sur lesquelles le développeur pourra avoir un impact sont :

  • Image : représente les librairies chargées par l’application.
  • Managed Heap : représente les tas alloués par la CLR .Net. Une augmentation incontrôlée de cette valeur peut indiquer une fuite mémoire.
  • Private : représente la mémoire non allouée par la CLR .Net. Par exemple, les données d’une image chargée au travers de Bitmap.FromFile seront dans cette zone mémoire.

Voici quatre captures d’écran de l’outil VMMap, représentant quatre états de la mémoire pour une application simple.

Après le chargement de l’application :

Utilisation mémoire initiale

On peut constater que la mémoire “Managed Heap” ainsi que “Private Data” sont respectivement de 2.3 et 25 Mo.

Après création de 10 tableaux de 1000000 bytes (non initialisés) :

Aprsè allocation de mémoire managée

La partie “Managed Heap” dans “Private Bytes” est maintenant de 109 Mo. Il s’agit uniquement de mémoire réservée, et non de mémoire utilisée ! C’est bien pour cela que le “Managed Heap” dans le “Working Set” est toujours de 2.5Mo.

Après l’initialisation de ces tableaux :

Après initialisation de ces tableaux

Cette fois ci, la taille du “Managed Heap” dans le “Working Set” a augmenté de manière significative : 108 Mo.

Après chargement de 300 images png de 40Ko.

Après chargement de fichiers PNG

Les données des images ont été allouées dans “Private data”, car System.Drawing.Bitmap utilise du code non managé.

Enfin, pour suivre la consommation mémoire d’une application, on pourra se baser sur les compteurs de performance suivants :

Compteurs de performance

comments edit

Une des dernières parties de cette série d’article concerne la sérialisation correcte des graphes d’objets, et surtout de leurs références. En effet, le tableau déclaré de la manière suivante :

var instance = new TestReference() { Str = "3" };
var array = new TestReference[] { instance, instance, instance, instance };

Ne doit pas sérialiser l’objet “Instance” quatre fois, mais une seule. La résolution de ce problème permettra également d’aborder les références cycliques :

var t1 = new TestReference() { Str = "1" };
var t2 = new TestReference() { Str = "2" };
 
t1.Reference = t2;
t2.Reference = t1;

Dans un premier temps, il faut pouvoir identifier les objets de manière unique. Dans un premier temps, j’ai régardé du coté de la classe GCHandlepour obtenir l’adresse mémoire des objets. Cette méthode ne s’est pas avérée adaptée : le Garbage Collector pouvant déplacer les objets et donc changer leur adresse mémoire.

En fait, le framework offre une classe toute faite pour identifier les objets : ObjectIDGenerator, qui permet d’obtenir un identifiant unique pour chaque objet passé en paramètre à la méthode GetId. Et en bonus, cette méthode indique même si il s’agit d’une instance déjà identifiée.

La modification de la classe DefaultObjectSerializer s’avère donc simple :

public override void Serialize(ExtendedBinaryWriter writer, object source, Type sourceType)
{
    bool firstTime;
 
    // generate unique id for object, in order not to save same object multiple times
    var key = idGenerator.GetId(source, out firstTime);
 
    writer.Write(firstTime);
    writer.Write(key);
 
    if (firstTime)
    {
        // inspect object
        foreach (var prop in sourceType.GetFields(BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Public).OrderBy(x => x.Name))
        {
            this.SerializeBase(prop.FieldType, prop.GetValue(source), writer);
        }
    }
}
 
public override object Deserialize(ExtendedBinaryReader source, object target, Type type)
{
    var firstTime = source.ReadBoolean();
    var key = source.ReadInt64();
 
    if (!firstTime)
    {
        return cache[key];
    }
    else
    {
        var destination = Activator.CreateInstance(type);
 
        // add instance to cache before deserializing properties, to untangle eventual cyclic dependencies
        cache.Add(key, destination);
 
        // inspect object
        foreach (var prop in type.GetFields(BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Public).OrderBy(x => x.Name))
        {
            var v = this.DeserializeBase(prop.FieldType, destination, source);
            prop.SetValue(destination, v);
        }
 
        return destination;
    }
}

En résumé, pour la sérialisation, on identifie chaque objet sérialisé, et si il est connu, on ne sérialise que son identifiant. Pour la désérialisation, on place les objets et leur identifiant dans un dictionnaire faisant office de cache.

La subtilité concerne les références cycliques : il faut placer l’objet dans le cache juste après sa création, car lorsque l’on va désérialiser les propriétés de l’objet, on pourrait rencontrer une référence vers un objet que l’on n’aurait pas encore désérialisé.

Comme toujours, les tests unitaires permettent de valider que les modifications n’entrainent pas de régressions.

Le code source de cet article est disponible sur GitHub.

comments edit

Après avoir refactorisé le code du Serializer, j’ai choisi d’implémenter la sérialisation des implémentations d’interfaces. En effet, si un objet possède des propriétés de type interface (IList par exemple), ce n'est pas pour autant qu'il ne doit pas pouvoir être sérialisable.

L’implémentation de la classe InterfaceSerializer s’est avérée plus simple que prévue : dans le principe, si un type de propriété est une interface, et que la valeur de cette propriété est non nulle, le type concret, obtenu via object.GetType() est sérialisé.

Ensuite, on appelle simplement le sérializer racine, en lui substituant le type de l’interface par le type de l’instance qui l’implémente.

public override void Serialize(ExtendedBinaryWriter writer, object source, Type sourceType)
{
    var st = source.GetType();
 
    // write implemented type
    writer.Write(st);
 
    // continue with implemented type and not interface
    base.Serialize(writer, source, st);
}

Le code source de cet article est disponible sur GitHub.