Register a SA Forums Account here!
JOINING THE SA FORUMS WILL REMOVE THIS BIG AD, THE ANNOYING UNDERLINED ADS, AND STUPID INTERSTITIAL ADS!!!

You can: log in, read the tech support FAQ, or request your lost password. This dumb message (and those ads) will appear on every screen until you register! Get rid of this crap by registering your own SA Forums Account and joining roughly 150,000 Goons, for the one-time price of $9.95! We charge money because it costs us money per month for bills, and since we don't believe in showing ads to our users, we try to make the money back through forum registrations.
 
  • Locked thread
ShadowHawk
Jun 25, 2000

CERTIFIED PRE OWNED TESLA OWNER

supercrooky posted:

Anyone have any suggestions for a memory efficient 2D array of ints with reasonable indexing speed? I've gone down the list of lists route (too much memory usage) and the numpy array route (slow indexing). My next thought is wrapping the builtin array object to have a 2D like interface.
What about numpy + numba?

Adbot
ADBOT LOVES YOU

QuarkJets
Sep 8, 2008

supercrooky posted:

Anyone have any suggestions for a memory efficient 2D array of ints with reasonable indexing speed? I've gone down the list of lists route (too much memory usage) and the numpy array route (slow indexing). My next thought is wrapping the builtin array object to have a 2D like interface.

What's the actual problem that has led you to needing a 2D array with faster indexing?

supercrooky
Sep 12, 2006

QuarkJets posted:

What's the actual problem that has led you to needing a 2D array with faster indexing?

Needleman-Wunsch/Smith-Waterman algorithms for protein alignment.

List of arrays is getting me 90% less memory usage for 10% additional runtime, so that's going to be good enough for now. Haven't tried numba yet.

Suspicious Dish
Sep 24, 2011

2020 is the year of linux on the desktop, bro
Fun Shoe
Is there something wrong with

Python code:
def get_array_at(arr, stride, x, y):
    return arr[y*stride + x]

A = [
  1, 2, 3, 4,
  5, 6, 7, 8,
  9, 10, 11, 12
]
get_array_at(A, 4, 1, 2)
But with numpy?

OnceIWasAnOstrich
Jul 22, 2006

The last time I implemented a dynamic programming algorithm in Python and cared about speed my fastest iteration involved numpy typed memoryviews and Cython.

Munkeymon
Aug 14, 2003

Motherfucker's got an
armor-piercing crowbar! Rigoddamndicu𝜆ous.



The only time I ever used Smith-Waterman my sequences were too short to get any benefit out of using numpy over a mostly naive implementation :\

Looking at it again, I could probably abuse a multi-level generator to speed it up some :unsmigghh:

Crosscontaminant
Jan 18, 2007

I've used dicts with tuple keys before.

Python code:
A = {
  (0,0):  1, (0,1):  2, (0,2):  3, (0,3):  4,
  (1,0):  5, (1,1):  6, (1,2):  7, (1,3):  8,
  (2,0):  9, (2,1): 10, (2,2): 11, (2,3): 12,
}
print(A[1,2])

OnceIWasAnOstrich
Jul 22, 2006

Crosscontaminant posted:

I've used dicts with tuple keys before.

Dicts are usually not the answer to a question involving the words "memory-efficient".

BigRedDot
Mar 6, 2008

OnceIWasAnOstrich posted:

Dicts are usually not the answer to a question involving the words "memory-efficient".

If the choice is between a 1000x1000 dense array with 100 non-zero values, or a cheap-o sparse array using a dict with 100 items, the dict wins.

OnceIWasAnOstrich
Jul 22, 2006

I suppose that was so broad a statement as to be wrong. Dicts will be memory efficient for things hash tables are good for, although not especially so compared to other implementations you can use from libraries. The constantly doubling memory use usually bites me when I care about memory. In any case, for most dynamic programming algorithms including Smith-Waterman, the arrays won't be especially sparse.

OnceIWasAnOstrich fucked around with this message at 23:35 on Aug 26, 2014

BigRedDot
Mar 6, 2008

OnceIWasAnOstrich posted:

I suppose that was so broad a statement as to be wrong. Dicts will be memory efficient for things hash tables are good for, although not especially so compared to other implementations you can use from libraries. The constantly doubling memory use usually bites me when I care about memory. In any case, for most dynamic programming algorithms including Smith-Waterman, the arrays won't be especially sparse.

They start out quadrupling, actually.

EricBauman
Nov 30, 2005

DOLF IS RECHTVAARDIG
This is all somewhat babby's first python, but can someone tell me why
code:
import time
import urllib
countdown = 200

while countdown > 0 :
    for url in open('listoflinks.txt') :
        
       filename = str(countdown) + '.html'
       urllib.urlretrieve(url, filename)    
       
       time.sleep(3)
       countdown = countdown - 1
    print "This prints once every 3 seconds - ", countdown   
doesn't print that message and the countdown every three seconds?
I'm pretty sure I indented it all correctly.

vikingstrike
Sep 23, 2007

whats happening, captain

EricBauman posted:

This is all somewhat babby's first python, but can someone tell me why
code:
import time
import urllib
countdown = 200

while countdown > 0 :
    for url in open('listoflinks.txt') :
        
       filename = str(countdown) + '.html'
       urllib.urlretrieve(url, filename)    
       
       time.sleep(3)
       countdown = countdown - 1
    print "This prints once every 3 seconds - ", countdown   
doesn't print that message and the countdown every three seconds?
I'm pretty sure I indented it all correctly.

The print statement is outside of the for loop, so it will only execute after every url in "listoflinks.txt" is done being processed. Indent the print line statement one more time if you want it to print after each url is processed.

EricBauman
Nov 30, 2005

DOLF IS RECHTVAARDIG

vikingstrike posted:

The print statement is outside of the for loop, so it will only execute after every url in "listoflinks.txt" is done being processed. Indent the print line statement one more time if you want it to print after each url is processed.

Hmm, I think i misinterpreted and misplanned something in my planning for this.
I wrote this a couple of weeks ago, then forgot about it because it didn't do what I wanted it to do.

Now, a couple of minutes later, I've found out that it also doesn't stop at zero. It happily continues into negative numbers. I'll try to put some break things and other stuff like that in there.

FoiledAgain
May 6, 2007

EricBauman posted:

Now, a couple of minutes later, I've found out that it also doesn't stop at zero. It happily continues into negative numbers. I'll try to put some break things and other stuff like that in there.

The for loop is where the subtraction happens, and the while loop is where it checks to see if your number is greater than 0. However, the while loop can't do that check until you've exited the for loop, and it's possible that your code inside the for loop continues to subtract into the negative numbers. You need some kind of check (like an if-statement) inside your for loop that prevents subtraction from happening if condition == 1 (and probably also breaks the loop).

edit: Do you even need a while-loop? How many times are you going to loop through that list of urls anyway? If only once, you don't need the while.

FoiledAgain fucked around with this message at 19:35 on Aug 27, 2014

vikingstrike
Sep 23, 2007

whats happening, captain

FoiledAgain posted:

edit: Do you even need a while-loop? How many times are you going to loop through that list of urls anyway? If only once, you don't need the while.

Yeah, I was about to post this. If you just need to run through the list once, then you just need the for loop there.

QuarkJets
Sep 8, 2008

EricBauman posted:

Hmm, I think i misinterpreted and misplanned something in my planning for this.
I wrote this a couple of weeks ago, then forgot about it because it didn't do what I wanted it to do.

Now, a couple of minutes later, I've found out that it also doesn't stop at zero. It happily continues into negative numbers. I'll try to put some break things and other stuff like that in there.

That's because the while loop is outside of the for loop. So the for loop will continue iterating over the listoflinks.txt, pausing 3 seconds between every line, until the entire listoflinks.txt has been read. After it's done iterating over the entire list, the while loop gets checked.

So if you had 300 entries in listoflinks.txt, you'd wind up with a countdown of -100 before the while loop ever checks to see if countdown is less than 0.

You also indented 4 spaces for one block and 7 spaces for another, you shouldn't indent by an odd number of spaces

If you want to stop iterating after countdown becomes negative, then you can do this:

Python code:
import time
import urllib
countdown = 200

for url in open('listoflinks.txt') :
        
    filename = str(countdown) + '.html'
    urllib.urlretrieve(url, filename)    
       
    time.sleep(3)
    countdown = countdown - 1
    print "This prints once every 3 seconds - ", countdown  
    if countdown < 0:
        break
This will stop when countdown reaches -1 or when the entire list has been iterated over, whichever happens first. If you want it to stop when countdown reaches 0, then you'll need to change "countdown < 0" to "countdown < 1" or "countdown <= 0"

ShadowHawk
Jun 25, 2000

CERTIFIED PRE OWNED TESLA OWNER

EricBauman posted:

Hmm, I think i misinterpreted and misplanned something in my planning for this.
I wrote this a couple of weeks ago, then forgot about it because it didn't do what I wanted it to do.

Now, a couple of minutes later, I've found out that it also doesn't stop at zero. It happily continues into negative numbers. I'll try to put some break things and other stuff like that in there.
One possible source of your confusion is that while loops only check their condition when you get to the end of the loop (or start it for the first time) -- they don't continuously monitor the truthiness of their condition.

EricBauman
Nov 30, 2005

DOLF IS RECHTVAARDIG

ShadowHawk posted:

One possible source of your confusion is that while loops only check their condition when you get to the end of the loop (or start it for the first time) -- they don't continuously monitor the truthiness of their condition.

Yeah, I think that's exactly what I was wrong about.

This whole project is so a colleague can just more or less automatically download the last X files from a list that automatically logs changed files. The sleep is to prevent the process from clogging up our unstable wireless. I could do it without the countdown and the while loop, but then I'd need to change the list to only contain X items, and since the list is also used by other people for other purposes (I imagined my colleague could copy it to the folder the files have to go in), that's not really a possibility.

Always fun, being the only non-techy team leader in an IT company and having to hack poo poo like this together together. Sure, I could ask a real programmer, but where's the fun in that?

Dren
Jan 5, 2001

Pillbug

EricBauman posted:

Yeah, I think that's exactly what I was wrong about.

This whole project is so a colleague can just more or less automatically download the last X files from a list that automatically logs changed files. The sleep is to prevent the process from clogging up our unstable wireless. I could do it without the countdown and the while loop, but then I'd need to change the list to only contain X items, and since the list is also used by other people for other purposes (I imagined my colleague could copy it to the folder the files have to go in), that's not really a possibility.

Always fun, being the only non-techy team leader in an IT company and having to hack poo poo like this together together. Sure, I could ask a real programmer, but where's the fun in that?

Is listoffiles.txt being appended to or prepended to?

Because if it's being appended to you're not getting the last 200 files, you're getting the first 200 files. You should also check if a file has already been downloaded before you download it.

And if you work with programmers isn't there one of them who would be willing to help you out instead of just writing it for you?

dirtycajun
Aug 27, 2004

SUCKING DICKS AND SQUEEZING TITTIES
Fixed the problem with the Windows COM ports, still not entirely sure what caused it.

dirtycajun fucked around with this message at 15:39 on Aug 28, 2014

dirtycajun
Aug 27, 2004

SUCKING DICKS AND SQUEEZING TITTIES
See above.

dirtycajun fucked around with this message at 15:39 on Aug 28, 2014

EricBauman
Nov 30, 2005

DOLF IS RECHTVAARDIG

Dren posted:

Is listoffiles.txt being appended to or prepended to?

Because if it's being appended to you're not getting the last 200 files, you're getting the first 200 files. You should also check if a file has already been downloaded before you download it.

And if you work with programmers isn't there one of them who would be willing to help you out instead of just writing it for you?

It's being prepended to, fortunately.

As to why I can't use programmers? Office politics, deadlines, stuff like that. I also want to learn to at least do some of this stuff myself, and I was working from home yesterday, looking at old projects that kind of dropped off my to do list.

Tacos Al Pastor
Jun 20, 2003

dirtycajun posted:

Fixed the problem with the Windows COM ports, still not entirely sure what caused it.

Not trying to be snarky. If you're not entirely sure what caused it, are you entirely sure of the solution?

Ccs
Feb 25, 2011


I'm just starting to learn Python to use in Maya and other CG programs to automate rigging and stuff like that. Python has started to make sense to me, and I'm learning the syntax okay, although a lot of the more abstract aspects of object oriented programming are still beyond my conceptual grasp.

One thing I don't get about python is why this works:

Python code:
def factorial(x):
    if x == 1:
       return 1
    else:
       return x * factorial (x-1)
That computes the factorial for numbers above 0, but I don't get why you can call the function inside of the function itself. It seems like that should cause a cycle error or something, what with the function depending on itself to produce the result. But it does work, so I'm not understanding something about how Python is structured.

ohrwurm
Jun 25, 2003

Ccs posted:

I'm just starting to learn Python to use in Maya and other CG programs to automate rigging and stuff like that. Python has started to make sense to me, and I'm learning the syntax okay, although a lot of the more abstract aspects of object oriented programming are still beyond my conceptual grasp.

One thing I don't get about python is why this works:

Python code:

def factorial(x):
    if x == 1:
       return 1
    else:
       return x * factorial (x-1)

That computes the factorial for numbers above 0, but I don't get why you can call the function inside of the function itself. It seems like that should cause a cycle error or something, what with the function depending on itself to produce the result. But it does work, so I'm not understanding something about how Python is structured.

This is called recursion and isn't specific to python.

salisbury shake
Dec 27, 2011
Anyone have success with bs4 and multiprocessing?

Python code:
from multiprocessing import Pool
from bs4 import BeautifulSoup as BS
import requests

url = 'http://yahoo.com'

with Pool(4) as pool:
    pgs = (requests.get(url).content for _ in range(8))
    wrapped_pgs = pool.map(BS, pgs)
shits itself with a max recursion limit reached RuntimeError, but replace the URL with say google.com and it works just fine. Replace it with the forum URL and get a different RuntimeError. Wrapping content serially works just fine for all urls involved.

If I break out lxml will I run into the same problem? Changing the bs4 backend to lxml still produces errors.


The bottle neck in the actual project are the handful of calls to the BeautifulSoup constructor that cost almost a second each on my machine.



Check out this link, it answers your question very specifically.

salisbury shake fucked around with this message at 18:04 on Aug 29, 2014

ShadowHawk
Jun 25, 2000

CERTIFIED PRE OWNED TESLA OWNER

Ccs posted:

I'm just starting to learn Python to use in Maya and other CG programs to automate rigging and stuff like that. Python has started to make sense to me, and I'm learning the syntax okay, although a lot of the more abstract aspects of object oriented programming are still beyond my conceptual grasp.

One thing I don't get about python is why this works:

Python code:
def factorial(x):
    if x == 1:
       return 1
    else:
       return x * factorial (x-1)
That computes the factorial for numbers above 0, but I don't get why you can call the function inside of the function itself. It seems like that should cause a cycle error or something, what with the function depending on itself to produce the result. But it does work, so I'm not understanding something about how Python is structured.

Here is a somewhat simpler explanation of recursion from the Think Python book: http://www.greenteapress.com/thinkpython/html/thinkpython006.html#toc59

BabyFur Denny
Mar 18, 2003
Wouldn't that function cause problems for any non-natural number?

qntm
Jun 17, 2009

BabyFur Denny posted:

Wouldn't that function cause problems for any non-natural number?

To say nothing of strings.

EAT THE EGGS RICOLA
May 29, 2008

I have a jinja2 template that generates a page which displays a list of fields and their values based on the fields in a jinja2 context object. For example, it might show the key "unique_id" and "12345" as the value

Instead of using this label, I need to label the field values based on a mapping contained in a separate API. The API returns a JSON object that shows the mapping from the key to the plain language label.

For example, instead of displaying the key as "unique_id", it would be displayed as "Internal Unique Identifier". There are about a hundred of these fields, and the non-technical people that need to modify the mapping at the other side of the API will be making regular changes to this, so I cannot hardcode the plain language field names.

What is the best way to make an API call and to pass the response object to a jinja2 template?

EAT THE EGGS RICOLA fucked around with this message at 16:20 on Aug 29, 2014

Haystack
Jan 23, 2005





Ok, so that sort of data mashing is not the sort of thing you want to be doing inside of your template (down that path lies php). Get your data into shape, then pass it into the template. In your case, you're basically mashing together two dicts, so here's an outline of what your code code might look like.

Python code:
 #If you want things to stay in some sort of order, use ordereddicts instead of plain dicts
value_map = {"unique_id" : "12345", "etc" : "etc"}
human_labels = get_human_lablels() #Your API call, deserialized into a {"value_map key":"human label"} dict

#There's shorter ways to do this mapping, but I left it long for readability
for_humans = {}
for key, value in value_map.iteritems():
    human_label = human_labels[key]
    for_humans[human_label] = value

#Now your template only needs to do a simple loop over the finalized data
template.render("my_template.jinja", values=for_humans))
Generally speaking, anything you pass into a template should already be finalized, or at worst have an API that resolves down to single calls.

Haystack fucked around with this message at 16:48 on Aug 29, 2014

EAT THE EGGS RICOLA
May 29, 2008

Haystack posted:

Ok, so that sort of data mashing is not the sort of thing you want to be doing inside of your template (down that path lies php). Get your data into shape, then pass it into the template. In your case, you're basically mashing together two dicts, so here's an outline of what your code code might look like.

Python code:
 #If you want things to stay in some sort of order, use ordereddicts instead of plain dicts
value_map = {"unique_id" : "12345", "etc" : "etc"}
human_labels = get_human_lablels() #Your API call, deserialized into a {"value_map key":"human label"} dict

#There's shorter ways to do this mapping, but I left it long for readability
for_humans = {}
for key, value in value_map.iteritems():
    human_label = human_labels[key]
    for_humans[human_label] = value

#Now your template only needs to do a simple loop over the finalized data
template.render("my_template.jinja", values=for_humans))
Generally speaking, anything you pass into a template should already be finalized, or at worst have an API that resolves down to single calls.

I was afraid of that. This is a monstrosity of an application that someone has been doing crazy things with for years, and I was hoping to avoid delving into the guts straightaway. Thanks!

SurgicalOntologist
Jun 17, 2004

I'm having some weird issues with sys.path... when creating new conda environments, it's adding ~/.local/lib to the path (which is where pip install --user puts stuff). I was not able to replicate this on another machine. Very confused. How do I even go about troubleshooting this?

E: wait it is happening on my other machine. Maybe a conda bug.

SurgicalOntologist fucked around with this message at 01:46 on Aug 30, 2014

SurgicalOntologist
Jun 17, 2004

Conda issue discussed here.

I'm trying to figure out how to fix it (one way is to always use the -s command-line flag, but that's annoying).

The Python docs say:

quote:

site.ENABLE_USER_SITE
Flag showing the status of the user site-packages directory. True means that it is enabled and was added to sys.path. False means that it was disabled by user request (with -s or PYTHONNOUSERSITE). None means it was disabled for security reasons (mismatch between user or group id and effective id) or by an administrator.

I'm interested in the phrase or by an administator. I'm an administrator (on my own machines), how do I disable it for a particular interpreter? Any ideas? (The environment variable is a non-starter because I need to disable it only for specific interpreters)

E: A second option, I suppose, is to stop using pip install --user and go back to sudo pip install for those unfortunate times when I need to install something system-wide. If the user-site directory doesn't exist, there's no problem. However, I don't like this because I make extensive use of the ~/.local prefix in general.

SurgicalOntologist fucked around with this message at 03:18 on Aug 30, 2014

Ccs
Feb 25, 2011


Alright here's another question from me, the newbie Python learner. In this code:

code:
score = {"a": 1, "c": 3, "b": 3, "e": 1, "d": 2, "g": 2, 
         "f": 4, "i": 1, "h": 4, "k": 5, "j": 8, "m": 3, 
         "l": 1, "o": 1, "n": 1, "q": 10, "p": 3, "s": 1, 
         "r": 1, "u": 1, "t": 1, "w": 4, "v": 4, "y": 4, 
         "x": 8, "z": 10}
         
def scrabble_score(word):
    word_score = 0
    word = word.lower()
    for letter in word:
        word_score += score[letter] 
    return word_score

print scrabble_score("a")
So this code adds up scrabble scores.

When you define the variable "letter" in "word", how does "letter" know that it's supposed to represent the key to the dictionary? Why doesn't it assign itself to the value of the key? Or the key and value together?
How is python this smart?

Ccs fucked around with this message at 17:06 on Aug 31, 2014

SurgicalOntologist
Jun 17, 2004

score[letter] takes the value of letter (which due to the line for letter in word, will be one letter of the word at a time) and uses that letter to look up a value in the dictionary score.

For example, score['a'] returns 1. If word starts with a, then on the first iteration of the loop, letter will be 'a', and score[letter] will return 1.

letter gets its values from word without reference to the dictionary score, if that's your confusion. If you try this with a word like 'abc123' you'll get an error when it tries to look up score['1']. In other words, Python isn't that smart, the code only works as long as the letters in word are keys in score. It's up to whoever wrote and uses this code to ensure that the letters are in the dictionary.

E: And because I can't resist, allow me to improve the code:

Python code:
SCORE_BY_LETTER = {
    "a": 1,
    "c": 3,
    "b": 3,
    "e": 1,
    "d": 2,
    "g": 2,
    "f": 4,
    "i": 1,
    "h": 4,
    "k": 5,
    "j": 8,
    "m": 3, 
    "l": 1,
    "o": 1,
    "n": 1,
    "q": 10,
    "p": 3,
    "s": 1,
    "r": 1,
    "u": 1,
    "t": 1,
    "w": 4,
    "v": 4,
    "y": 4,
    "x": 8,
    "z": 10,
}

def scrabble_score(word):
    return sum(SCORE_BY_LETTER[letter] for letter in word.lower())

SurgicalOntologist fucked around with this message at 17:51 on Aug 31, 2014

BigRedDot
Mar 6, 2008

As long as we are "improving":
code:
LETTER_BY_SCORE = {
    1: "aeilnorstu",
    2: "dg",
    3: "bcmp",
    4: "fhvwy",
    5: "k",
    8: "jx",
    10: "qz",
}

def scrabble_score(word):
    return sum(score for letter in word for score, letters in LETTER_BY_SCORE.items() if letter in letters)
Never actually used a nested generator before, couldn't resist...

Symbolic Butt
Mar 22, 2009

(_!_)
Buglord
It's a cool idea and I know you're just trying things out but I like SurgicalOntologist's version better. It's a little faster and more straightforward.

Adbot
ADBOT LOVES YOU

BigRedDot
Mar 6, 2008

Symbolic Butt posted:

It's a cool idea and I know you're just trying things out but I like SurgicalOntologist's version better. It's a little faster and more straightforward.

Oh, I do too, hence the scare quotes. :) Let's go another direction and make the ultra-flexible enterprise scorer that can perform arbitrary accumulations and transforms on the input with configurable decoders:

code:
''' The score.py module provides functions for generalized word 
scoring, as well as specialized scorers for particular rulesets.

'''

def score_word(word, decode, transform=lambda x: x, accum=sum):
    ''' Score an input word according to given decode, transform, and 
    accumulation policies.

    Args:
        word (str): 
            a word to score
        decode (callable): 
            callable taking one letter as input that maps the letter to 
            its score
        transform(callable, optional) : 
            callable that performs any necessary transformation on each 
            letter before decoding (default: identity)
        accum (callable, optional): 
            callable the reduces a sequence of letter scores into a 
            final score (default: sum)

    Returns:
        float : score

    Examples:

    >>> score_word("za", decode=lambda x: 2)
    4
    >>>

    '''
    return accum(decode(letter) for letter in transform(word))

#
# Scrabble (tm) specific scoring functions
# 

_SCRABBLE_SCORE_BY_LETTER = {
    "c": 3,
    "b": 3,
    "d": 2,
    "g": 2,
    "f": 4,
    "h": 4,
    "k": 5,
    "j": 8,
    "m": 3, 
    "q": 10,
    "p": 3,
    "w": 4,
    "v": 4,
    "y": 4,
    "x": 8,
    "z": 10,
}


def scrabble_word(word):
    ''' Score a word according to Scrabble (tm) rules.

    Args:
        word (str) : 
            a word to score

    Returns:
        float : score

    Examples:

    >>> scrabble_word("za")
    11
    >>>

    '''
    return score_word(
        word, 
        decode=lambda x: _SCRABBLE_SCORE_BY_LETTER.get(x, 1), 
        transform=lambda x: x.lower()
    )

if __name__ == '__main__':
    import doctest
    doctest.testmod()
Sorry, it's Labor Day, and I'm, er... uh, working? I leave it as an exercise to the reader to extend this and define an accumulator to handle scoring (double, triple)-(letter, word) modifiers. Don't forget to update the doctests, and re-run Sphinx to generate new API documentation (you'll need sphinx-napoleon to handle the Google style docstrings). Pull requests welcome!

Edit: Unicode support is an open issue; see the GH tracker.

BigRedDot fucked around with this message at 16:10 on Sep 1, 2014

  • Locked thread